├── .dockerignore ├── example.gif ├── jest.config.js ├── .travis.yml ├── src ├── index.js ├── config │ └── lighthouse.json └── lib │ ├── index.js │ └── spec.js ├── codecov.yml ├── release.sh ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── package.json ├── Dockerfile ├── LICENSE ├── example └── config │ └── lighthouse.json └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyney123/performance-budgets/HEAD/example.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coveragePathIgnorePatterns: ["/node_modules/"] 3 | }; 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | install: 6 | - npm install -g codecov 7 | - npm install 8 | script: 9 | - npm test 10 | - codecov -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const lighthouseBudgets = require("./lib/"); 2 | 3 | const main = async () => { 4 | try { 5 | await lighthouseBudgets(); 6 | process.exit(0); 7 | } catch (error) { 8 | console.log(error); 9 | process.exit(1); 10 | } 11 | }; 12 | 13 | main(); 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # Fail the status if coverage drops by >= 5% 6 | threshold: 5 7 | patch: 8 | default: 9 | threshold: 5 10 | 11 | comment: 12 | layout: diff 13 | require_changes: yes 14 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | # SET THE FOLLOWING VARIABLES 3 | # docker hub username 4 | USERNAME=boyney123 5 | # image name 6 | IMAGE=performance-budgets 7 | # run build 8 | docker build -t $USERNAME/$IMAGE:latest . 9 | # tag it 10 | git tag -a "$1" -m "version $1" 11 | git push 12 | git push --tags 13 | 14 | docker tag $USERNAME/$IMAGE:latest $USERNAME/$IMAGE:$1 15 | # push it 16 | docker push $USERNAME/$IMAGE:latest 17 | docker push $USERNAME/$IMAGE:$1 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What you did: 2 | 3 | ## What happened: 4 | 5 | 6 | 7 | ## Problem description: 8 | 9 | 10 | 11 | ## Suggested solution: 12 | 13 | 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What you did: 2 | 3 | ## What happened: 4 | 5 | 6 | 7 | ## Problem description: 8 | 9 | 10 | 11 | ## Suggested solution: 12 | 13 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Node ### 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # nyc test coverage 17 | .nyc_output 18 | 19 | # Compiled binary addons (https://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directories 23 | node_modules/ 24 | 25 | # dotenv environment variables file 26 | .env 27 | .env.test -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lighthouse-github-action", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "NODE_ENV=test jest --coverage --silent", 8 | "test:watch": "NODE_ENV=test jest --watch --notify --notifyMode=change", 9 | "start": "node src/index.js" 10 | }, 11 | "jest": { 12 | "collectCoverageFrom": [ 13 | "src/*.{js}" 14 | ] 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "chalk": "^2.4.2", 21 | "chrome-launcher": "^0.10.7", 22 | "fs-extra": "^8.0.1", 23 | "lighthouse": "^5.0.0" 24 | }, 25 | "devDependencies": { 26 | "jest": "^24.8.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-slim 2 | 3 | LABEL maintainer="David Boyne " 4 | 5 | WORKDIR /usr/src/performance-budgets 6 | 7 | # Install latest chrome dev package. 8 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 9 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 10 | && apt-get update \ 11 | && apt-get install -y google-chrome-unstable --no-install-recommends \ 12 | && rm -rf /var/lib/apt/lists/* \ 13 | && rm -rf /src/*.deb 14 | 15 | COPY src /usr/src/performance-budgets/src 16 | COPY package.json /usr/src/performance-budgets 17 | 18 | RUN npm install 19 | 20 | ENTRYPOINT [ "npm", "start" ] 21 | -------------------------------------------------------------------------------- /src/config/lighthouse.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "lighthouse:full", 3 | "settings": { 4 | "budgets": [ 5 | { 6 | "resourceSizes": [ 7 | { 8 | "resourceType": "script", 9 | "budget": 50 10 | }, 11 | { 12 | "resourceType": "image", 13 | "budget": 10 14 | }, 15 | { 16 | "resourceType": "third-party", 17 | "budget": 40 18 | }, 19 | { 20 | "resourceType": "total", 21 | "budget": 500 22 | }, 23 | { 24 | "resourceType": "stylesheet", 25 | "budget": 200 26 | } 27 | ], 28 | "resourceCounts": [ 29 | { 30 | "resourceType": "third-party", 31 | "budget": 60 32 | }, 33 | { 34 | "resourceType": "stylesheet", 35 | "budget": 20 36 | }, 37 | { 38 | "resourceType": "total", 39 | "budget": 50 40 | } 41 | ] 42 | } 43 | ] 44 | }, 45 | "isCustom": false 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) David Boyne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/config/lighthouse.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "lighthouse:full", 3 | "settings": { 4 | "budgets": [ 5 | { 6 | "resourceSizes": [ 7 | { 8 | "resourceType": "script", 9 | "budget": 50 10 | }, 11 | { 12 | "resourceType": "image", 13 | "budget": 10 14 | }, 15 | { 16 | "resourceType": "third-party", 17 | "budget": 40 18 | }, 19 | { 20 | "resourceType": "total", 21 | "budget": 500 22 | }, 23 | { 24 | "resourceType": "stylesheet", 25 | "budget": 200 26 | } 27 | ], 28 | "resourceCounts": [ 29 | { 30 | "resourceType": "third-party", 31 | "budget": 60 32 | }, 33 | { 34 | "resourceType": "stylesheet", 35 | "budget": 20 36 | }, 37 | { 38 | "resourceType": "total", 39 | "budget": 50 40 | } 41 | ] 42 | } 43 | ] 44 | }, 45 | "isCustom": true 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | const lighthouse = require("lighthouse"); 2 | const chromeLauncher = require("chrome-launcher"); 3 | const fs = require("fs-extra"); 4 | const path = require("path"); 5 | const chalk = require("chalk"); 6 | const log = console.log; 7 | 8 | const getLightHouseConfig = () => { 9 | return fs.readJSONSync(path.join(__dirname, "../config/lighthouse.json")); 10 | }; 11 | 12 | function launchChromeAndRunLighthouse(url, opts, config = null) { 13 | return chromeLauncher.launch({ chromeFlags: opts.chromeFlags }).then(chrome => { 14 | opts.port = chrome.port; 15 | opts.output = "json"; 16 | 17 | const { isCustom, ...lightHouseConfig } = getLightHouseConfig(); 18 | 19 | if (!isCustom) { 20 | log(` 21 | ------- 22 | Using example configuration for lighthouse. 23 | You can configure your own lighthouse rules & budgets, read the documentation for more information. 24 | https://github.com/boyney123/performance-budgets 25 | ------- 26 | `); 27 | } 28 | 29 | return lighthouse(url, opts, lightHouseConfig).then(results => { 30 | return chrome.kill().then(() => results.lhr); 31 | }); 32 | }); 33 | } 34 | 35 | const opts = { 36 | chromeFlags: ["--disable-gpu", "--headless", "--no-zygote", "--no-sandbox", "--headless"] 37 | }; 38 | 39 | const main = async () => { 40 | try { 41 | const url = process.argv[2]; 42 | 43 | if (!url) { 44 | log(chalk.red("Please provide a url")); 45 | return Promise.reject("Please provide a valid url"); 46 | } 47 | 48 | log(`Requesting lighthouse data for ${chalk.green(url)}`); 49 | 50 | const data = await launchChromeAndRunLighthouse(url, opts); 51 | const budgets = data["audits"]["performance-budget"]; 52 | const { details: { items = [] } = {} } = budgets; 53 | 54 | const successfulAudits = items.filter(({ sizeOverBudget, countOverBudget }) => { 55 | return !sizeOverBudget && !countOverBudget; 56 | }); 57 | 58 | const failedAudits = items.filter(({ sizeOverBudget, countOverBudget }) => { 59 | return sizeOverBudget || countOverBudget; 60 | }); 61 | 62 | const isValid = successfulAudits.length === items.length; 63 | 64 | if (!isValid) { 65 | const failedRequestCountAudits = failedAudits.filter(audit => audit.countOverBudget !== undefined); 66 | const failedSizeAudits = failedAudits.filter(audit => audit.sizeOverBudget !== undefined); 67 | 68 | if (failedRequestCountAudits.length) { 69 | log(chalk.red("----- Failed resource count budget audits ------")); 70 | failedRequestCountAudits.forEach(({ label, requestCount, size, sizeOverBudget, countOverBudget } = {}) => { 71 | const expectedCount = requestCount - countOverBudget.split(" requests")[0]; 72 | log(`${chalk.green(label)}: Expected ${chalk.green(expectedCount)} total number of requests but got ${chalk.red(countOverBudget)}`); 73 | }); 74 | } 75 | 76 | if (failedSizeAudits.length) { 77 | log(chalk.red("----- Failed resource size budget audits ------")); 78 | failedSizeAudits.forEach(({ label, requestCount, size, sizeOverBudget, countOverBudget } = {}) => { 79 | const expectedSize = Math.round((size - sizeOverBudget) / 1024); 80 | const actual = Math.round(size / 1024); 81 | const overBy = Math.round(sizeOverBudget / 1024); 82 | log(`${chalk.green(label)}: Expected ${chalk.green(expectedSize + "kb")} download size but got ${chalk.red(actual + "kb")}`); 83 | }); 84 | } 85 | return Promise.reject("Budgets broken"); 86 | } 87 | 88 | log("All budgets passed. ✔"); 89 | return Promise.resolve(); 90 | } catch (error) { 91 | log(error); 92 | return Promise.reject("Failed to get lighthouse data"); 93 | } 94 | }; 95 | 96 | module.exports = main; 97 | -------------------------------------------------------------------------------- /src/lib/spec.js: -------------------------------------------------------------------------------- 1 | const lighthouseBudgets = require("./"); 2 | const chromeLauncher = require("chrome-launcher"); 3 | const lighthouse = require("lighthouse"); 4 | const config = require("../config/lighthouse.json"); 5 | 6 | jest.mock("chrome-launcher", () => { 7 | return { 8 | launch: jest.fn(() => Promise.resolve({ kill: () => Promise.resolve() })) 9 | }; 10 | }); 11 | 12 | jest.mock("lighthouse", () => { 13 | return jest.fn(() => Promise.resolve()); 14 | }); 15 | 16 | const buildBudgetData = (item = { label: "Third-Party", requestCount: 1 }) => ({ 17 | lhr: { 18 | audits: { 19 | "performance-budget": { 20 | details: { 21 | items: [ 22 | { 23 | label: "Script", 24 | requestCount: 2 25 | }, 26 | item 27 | ] 28 | } 29 | } 30 | } 31 | } 32 | }); 33 | 34 | let processExit, processArgv; 35 | 36 | describe("performance-budgets", () => { 37 | beforeAll(() => { 38 | processArgv = process.argv; 39 | }); 40 | beforeEach(() => { 41 | processExit = jest.spyOn(process, "exit").mockImplementation(() => {}); 42 | console.log("here", processArgv); 43 | process.argv = processArgv; 44 | }); 45 | 46 | it("lighthouse is launched with the given url, default chrome flags and default configuration file", async () => { 47 | lighthouse.mockImplementation(() => Promise.resolve(buildBudgetData())); 48 | process.argv = ["", "", "https://example.com"]; 49 | ({ isCustom, ...defaultConfig } = config); 50 | 51 | await lighthouseBudgets(); 52 | 53 | expect(lighthouse.mock.calls[0][0]).toEqual("https://example.com"); 54 | expect(lighthouse.mock.calls[0][1]).toEqual({ chromeFlags: ["--disable-gpu", "--headless", "--no-zygote", "--no-sandbox", "--headless"], port: undefined, output: "json" }); 55 | expect(lighthouse.mock.calls[0][2]).toEqual(defaultConfig); 56 | }); 57 | 58 | it("the script fails when request budgets are broken", async () => { 59 | lighthouse.mockImplementation(() => Promise.resolve(buildBudgetData({ label: "Third-Party", requestCount: 3, countOverBudget: "2 requests" }))); 60 | process.argv = ["", "", "https://example.com"]; 61 | ({ isCustom, ...defaultConfig } = config); 62 | return expect(lighthouseBudgets()).rejects.toEqual("Budgets broken"); 63 | }); 64 | 65 | it("the script fails when size budgets are broken", async () => { 66 | lighthouse.mockImplementation(() => Promise.resolve(buildBudgetData({ label: "Third-Party", requestCount: 1, sizeOverBudget: 220 }))); 67 | process.argv = ["", "", "https://example.com"]; 68 | ({ isCustom, ...defaultConfig } = config); 69 | 70 | return expect(lighthouseBudgets()).rejects.toEqual("Budgets broken"); 71 | }); 72 | 73 | it("the script is successful when request or size budgets are not broken", async () => { 74 | lighthouse.mockImplementation(() => Promise.resolve(buildBudgetData())); 75 | process.argv = ["", "", "https://example.com"]; 76 | ({ isCustom, ...defaultConfig } = config); 77 | 78 | await lighthouseBudgets(); 79 | 80 | expect(lighthouse.mock.calls[0][0]).toEqual("https://example.com"); 81 | expect(lighthouse.mock.calls[0][1]).toEqual({ chromeFlags: ["--disable-gpu", "--headless", "--no-zygote", "--no-sandbox", "--headless"], port: undefined, output: "json" }); 82 | expect(lighthouse.mock.calls[0][2]).toEqual(defaultConfig); 83 | }); 84 | 85 | it("when not url is given the process with exit", () => { 86 | process.argv = []; 87 | return expect(lighthouseBudgets()).rejects.toEqual("Please provide a valid url"); 88 | }); 89 | 90 | it("the whole process will exit if chrome launcher fails", () => { 91 | chromeLauncher.launch.mockImplementation(() => Promise.reject()); 92 | return expect(lighthouseBudgets()).rejects.toEqual("Failed to get lighthouse data"); 93 | }); 94 | 95 | it("the whole process will exit if lighthouse fails", () => { 96 | lighthouse.mockImplementation(() => Promise.reject()); 97 | return expect(lighthouseBudgets()).rejects.toEqual("Failed to get lighthouse data"); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

performance-budgets: Keep your apps ⚡ with budgets

4 | 5 |

No more excuses not to set performance budgets. performance-budgets gives you a simple way to check and stay on top of performance. Easy to run and configure with your custom budgets.

6 | 7 |
8 | 9 | [![Travis](https://img.shields.io/travis/boyney123/performance-budgets/master.svg)](https://travis-ci.org/boyney123/performance-budgets) 10 | [![CodeCov](https://codecov.io/gh/boyney123/performance-budgets/branch/master/graph/badge.svg?token=AoXW3EFgMP)](https://codecov.io/gh/boyney123/performance-budgets) 11 | [![MIT License][license-badge]][license] 12 | [![PRs Welcome][prs-badge]][prs] 13 | 14 | [![Watch on GitHub][github-watch-badge]][github-watch] 15 | [![Star on GitHub][github-star-badge]][github-star] 16 | [![Tweet][twitter-badge]][twitter] 17 | 18 | [Donate ☕](https://www.paypal.me/boyney123/5) 19 | 20 |
21 | 22 | ### _Check budgets with one command..._ 23 | 24 | ```sh 25 | docker run --rm boyney123/performance-budgets https://example.com 26 | ``` 27 | 28 | header 29 | 30 |

Features: Set performance budgets, override lighthouse configuration, easily run on CI, and more...

31 | 32 | [Read the Docs](https://performance-budgets.netlify.com/) | [Edit the Docs](https://github.com/boyney123/performance-budgets) 33 | 34 |
35 | 36 |
37 | 38 | ## The problem 39 | 40 | Every feature we add has an impact on the performance of our applications and can effect the end user experience. Performance budgets are a great way to keep your applications fast. 41 | 42 | There is currently a lot of work going on around web performance and some new awesome features coming out. 43 | 44 | - https://web.dev/performance-budgets-101/ 45 | - https://addyosmani.com/blog/performance-budgets/ 46 | - https://web.dev/your-first-performance-budget/ 47 | - https://web.dev/incorporate-performance-budgets-into-your-build-tools/ 48 | 49 | [Addy Osmani has written a great article on performance budgets](https://addyosmani.com/blog/performance-budgets/), and he says... 50 | 51 | > Performance budgets usher a culture of accountability that enable stakeholders to weigh the impact to user-centric metrics of each change to a site. Talk to your organization and see if you can get by in to adopt performance budgets for your projects. If it's worth getting fast, it's worth staying fast. ❤️ 52 | 53 | [Lighthouse](https://developers.google.com/web/tools/lighthouse/) has recently come out with a [great feature](https://developers.google.com/web/tools/lighthouse/audits/budgets) that allows you to capture and set budgets on a given website. 54 | 55 | ## This solution 56 | 57 | `performance-budgets` was built from inspiration from these articles and built to give developers an easy way to get started with performance budgets. 58 | 59 | `performance-budgets` was built with continuous-integration in mind, allowing developers to run one command to check budgets against any given url. 60 | 61 | _In the future we hope to add more features to the project and start to monitor other stats._ 62 | 63 | ## Documentation 64 | 65 | - [Getting Started](https://performance-budgets.netlify.com/docs/getting-started/installation) 66 | - [Setting up Custom Budgets](https://performance-budgets.netlify.com/docs/getting-started/config) 67 | - [Understanding Budgets](https://performance-budgets.netlify.com/docs/getting-started/config#understanding-budgets) 68 | - [Contributing](https://performance-budgets.netlify.com/docs/contributing/contributing) 69 | 70 | ## Tools 71 | 72 | - [lighthouse](https://github.com/GoogleChrome/lighthouse) 73 | - [chrome-launcher](https://github.com/GoogleChrome/chrome-launcher) 74 | 75 | ### Testing 76 | 77 | - [jest](https://jestjs.io/) 78 | 79 | ## Contributing 80 | 81 | If you have any questions, features or issues please raise any issue or pull requests you like. 82 | 83 | [spectrum-badge]: https://withspectrum.github.io/badge/badge.svg 84 | [spectrum]: https://spectrum.chat/explore-tech 85 | [license-badge]: https://img.shields.io/github/license/boyney123/performance-budgets.svg 86 | [license]: https://github.com/boyney123/performance-budgets/blob/master/LICENSE 87 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 88 | [prs]: http://makeapullrequest.com 89 | [github-watch-badge]: https://img.shields.io/github/watchers/boyney123/performance-budgets.svg?style=social 90 | [github-watch]: https://github.com/boyney123/performance-budgets/watchers 91 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20performance-budgets%20by%20%40boyney123%20https%3A%2F%2Fgithub.com%2Fboyney123%2Fperformance-budgets%20%F0%9F%91%8D 92 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/boyney123/performance-budgets.svg?style=social 93 | [github-star-badge]: https://img.shields.io/github/stars/boyney123/performance-budgets.svg?style=social 94 | [github-star]: https://github.com/boyney123/performance-budgets/stargazers 95 | 96 | # Donating 97 | 98 | If you find this tool useful, feel free to buy me a ☕ 👍 99 | 100 | [Buy a drink](https://www.paypal.me/boyney123/5) 101 | 102 | # License 103 | 104 | MIT. 105 | --------------------------------------------------------------------------------