diff --git a/README.md b/README.md index 898ff286..c0354cb9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ if the cost exceeds the limit. * Can calculate **the time** it would take a browser to download and **execute** your JS. Time is a much more accurate and understandable metric compared to the size in bytes. + Additionally, you can [customize time plugin] via config + for every check with network speed, latency and so on. * Calculations include **all dependencies and polyfills** used in your JS. @@ -50,6 +52,7 @@ We are using [Statoscope] for this analysis. [Statoscope]: https://github.com/statoscope/statoscope [cult-img]: http://cultofmartians.com/assets/badges/badge.svg [cult]: http://cultofmartians.com/tasks/size-limit-config.html +[customize time plugin]: https://github.com/ai/size-limit/packages/time#customize-network-speed ## Who Uses Size Limit diff --git a/fixtures/time-latency/index.js b/fixtures/time-latency/index.js new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/time-latency/package.json b/fixtures/time-latency/package.json new file mode 100644 index 00000000..fa0b1483 --- /dev/null +++ b/fixtures/time-latency/package.json @@ -0,0 +1,21 @@ +{ + "private": true, + "name": "time-latency", + "devDependencies": { + "@size-limit/time": ">= 0.0.0", + "size-limit": ">= 0.0.0" + }, + "size-limit": [ + { + "path": "index.js", + "time": { + "latency": "200 ms" + } + }, + { + "time": { + "latency": "2.35 s" + } + } + ] +} diff --git a/fixtures/time-network-speed/index.js b/fixtures/time-network-speed/index.js new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/time-network-speed/package.json b/fixtures/time-network-speed/package.json new file mode 100644 index 00000000..0689bdf2 --- /dev/null +++ b/fixtures/time-network-speed/package.json @@ -0,0 +1,21 @@ +{ + "private": true, + "name": "time-network-speed", + "devDependencies": { + "@size-limit/time": ">= 0.0.0", + "size-limit": ">= 0.0.0" + }, + "size-limit": [ + { + "path": "index.js", + "time": { + "networkSpeed": "20 B" + } + }, + { + "time": { + "networkSpeed": "20 kB" + } + } + ] +} diff --git a/packages/size-limit/calc.js b/packages/size-limit/calc.js index 98343354..5a6987a7 100644 --- a/packages/size-limit/calc.js +++ b/packages/size-limit/calc.js @@ -46,7 +46,7 @@ export default async function calc(plugins, config, createSpinner) { check.passed = check.sizeLimit >= check.size } if (typeof check.timeLimit !== 'undefined') { - check.passed = check.timeLimit >= check.time + check.passed = check.timeLimit >= check.totalTime } if (check.files && !check.files.length && check.path) { check.missed = true diff --git a/packages/size-limit/create-reporter.js b/packages/size-limit/create-reporter.js index acd7124f..afbf95fa 100644 --- a/packages/size-limit/create-reporter.js +++ b/packages/size-limit/create-reporter.js @@ -153,12 +153,14 @@ function createHumanReporter(process, isSilentMode = false) { rows.push(['Size', sizeString, sizeNote]) } if (typeof check.loadTime !== 'undefined') { - rows.push(['Loading time', formatTime(check.loadTime), 'on slow 3G']) + let description = + (check.time && check.time.loadingMessage) || 'on slow 3G' + rows.push(['Loading time', formatTime(check.loadTime), description]) } if (typeof check.runTime !== 'undefined') { rows.push( ['Running time', formatTime(check.runTime), 'on Snapdragon 410'], - ['Total time', formatTime(check.time)] + ['Total time', formatTime(check.totalTime)] ) } diff --git a/packages/size-limit/get-config.js b/packages/size-limit/get-config.js index 872eccf2..6ae51515 100644 --- a/packages/size-limit/get-config.js +++ b/packages/size-limit/get-config.js @@ -28,6 +28,7 @@ let OPTIONS = { name: true, path: true, running: 'time', + time: 'time', uiReports: 'webpack', webpack: 'webpack' } @@ -42,6 +43,14 @@ function isStringsOrUndefined(value) { return type === 'undefined' || type === 'string' || isStrings(value) } +function endsWithMs(value) { + return / ?ms/i.test(value) +} + +function endsWithS(value) { + return / ?s/i.test(value) +} + function checkChecks(plugins, checks) { if (!Array.isArray(checks)) { throw new SizeLimitError('noArrayConfig') @@ -181,9 +190,9 @@ export default async function getConfig(plugins, process, args, pkg) { if (!check.name) check.name = toName(check.entry || check.files, config.cwd) if (args.limit) check.limit = args.limit if (check.limit) { - if (/ ?ms/i.test(check.limit)) { + if (endsWithMs(check.limit)) { check.timeLimit = parseFloat(check.limit) / 1000 - } else if (/ ?s/i.test(check.limit)) { + } else if (endsWithS(check.limit)) { check.timeLimit = parseFloat(check.limit) } else { check.sizeLimit = bytes.parse(check.limit) @@ -215,6 +224,19 @@ export default async function getConfig(plugins, process, args, pkg) { } check.import = imports } + if (check.time) { + let { latency, networkSpeed } = check.time + if (latency) { + if (endsWithMs(latency)) { + check.time.latency = parseFloat(latency) / 1000 + } else { + check.time.latency = parseFloat(latency) || 0 + } + } + if (networkSpeed) { + check.time.networkSpeed = bytes.parse(networkSpeed) + } + } } return config diff --git a/packages/size-limit/index.d.ts b/packages/size-limit/index.d.ts index 7c1bf9f9..3a86553a 100644 --- a/packages/size-limit/index.d.ts +++ b/packages/size-limit/index.d.ts @@ -96,6 +96,38 @@ export interface Check { * With `false` it will disable webpack. */ webpack?: boolean + + /** + * Options for `@size-limit/time` plugin. + */ + time?: TimeOptions +} + +/** + * Represents the options for the size-limit check time property to customize `@size-limit/time` plugin. + */ +export interface TimeOptions { + /** + * A network speed to calculate loading time of files. + * It should be a string with a number and unit, separated by a space. + * Format: `100 B`, `10 kB`. + * @default "50 kB" + */ + networkSpeed?: string + + /** + * Delay for calculating loading time that simulates network latency + * It should be a string with a number and unit, separated by a space. + * Format: `500 ms`, `1 s`. + * @default: "0" + */ + latency: string + + /** + * A message for loading time details + * @default "on slow 3G" + */ + loadingMessage?: string } export type SizeLimitConfig = Check[] diff --git a/packages/size-limit/test/__snapshots__/create-reporter.test.js.snap b/packages/size-limit/test/__snapshots__/create-reporter.test.js.snap index 958d7f04..ecd7eee9 100644 --- a/packages/size-limit/test/__snapshots__/create-reporter.test.js.snap +++ b/packages/size-limit/test/__snapshots__/create-reporter.test.js.snap @@ -137,6 +137,22 @@ exports[`renders list of success checks in silent mode 1`] = ` " `; +exports[`renders loading time with custom message from time options for every check 1`] = ` +" + loading message 1 + Size: 10 B + Loading time: 200 ms for ~1000 users per month + Running time: 400 ms on Snapdragon 410 + Total time: 1.4 s + + loading message 2 + Loading time: 200 ms on slow 3G + Running time: 300 ms on Snapdragon 410 + Total time: NaN ms + +" +`; + exports[`renders result for file with gzip 1`] = ` " Size limit: 99 B diff --git a/packages/size-limit/test/__snapshots__/run.test.js.snap b/packages/size-limit/test/__snapshots__/run.test.js.snap index 538ac4f0..7f47fa14 100644 --- a/packages/size-limit/test/__snapshots__/run.test.js.snap +++ b/packages/size-limit/test/__snapshots__/run.test.js.snap @@ -90,7 +90,7 @@ exports[`shows debug 1`] = ` "size": 123, "loadTime": 0.01, "runTime": 1, - "time": 1.01, + "totalTime": 1.01, "passed": true } ], diff --git a/packages/size-limit/test/create-reporter.test.js b/packages/size-limit/test/create-reporter.test.js index 9e04926c..5fb18ec4 100644 --- a/packages/size-limit/test/create-reporter.test.js +++ b/packages/size-limit/test/create-reporter.test.js @@ -31,7 +31,7 @@ it('renders results', () => { name: 'limitless', runTime: 0.5, size: 10, - time: 0.6 + totalTime: 0.6 }, { loadTime: 1, @@ -40,7 +40,7 @@ it('renders results', () => { runTime: 2, size: 102400, sizeLimit: 102400, - time: 3 + totalTime: 3 }, { gzip: false, @@ -49,8 +49,8 @@ it('renders results', () => { passed: true, runTime: 2, size: 102400, - time: 3, - timeLimit: 4 + timeLimit: 4, + totalTime: 3 } ] }) @@ -69,7 +69,7 @@ it('renders list of success checks in silent mode', () => { name: 'limitless', runTime: 0.5, size: 10, - time: 0.6 + totalTime: 0.6 }, { loadTime: 1, @@ -78,7 +78,7 @@ it('renders list of success checks in silent mode', () => { runTime: 2, size: 102400, sizeLimit: 102400, - time: 3 + totalTime: 3 } ] }, @@ -132,7 +132,7 @@ it('renders list of failed and success checks in silent mode', () => { name: 'limitless', runTime: 0.5, size: 10, - time: 0.6 + totalTime: 0.6 }, { loadTime: 1, @@ -141,7 +141,7 @@ it('renders list of failed and success checks in silent mode', () => { runTime: 2, size: 102400, sizeLimit: 102400, - time: 3 + totalTime: 3 }, { name: 'big fail', @@ -291,8 +291,8 @@ it('renders config-less result', () => { passed: false, runTime: 0.3, size: 1000, - time: 0.5, - timeLimit: 0.5 + timeLimit: 0.5, + totalTime: 0.5 } ], failed: true @@ -317,8 +317,8 @@ it('renders JSON results', () => { path: '/b', runTime: 0.3, size: 1000, - time: 0.5, - timeLimit: 10 + timeLimit: 10, + totalTime: 0.5 } ], failed: true @@ -372,3 +372,28 @@ it('renders Webpack stats help message', () => { }) ).toMatchSnapshot() }) + +it('renders loading time with custom message from time options for every check', () => { + expect( + results(['time'], { + checks: [ + { + loadTime: 0.2, + name: 'loading message 1', + passed: true, + runTime: 0.4, + size: 10, + time: { loadingMessage: 'for ~1000 users per month' }, + totalTime: 1.4 + }, + { + loadTime: 0.2, + name: 'loading message 2', + passed: true, + runTime: 0.3, + time: { loadingMessage: '' } + } + ] + }) + ).toMatchSnapshot() +}) diff --git a/packages/size-limit/test/get-config.test.js b/packages/size-limit/test/get-config.test.js index 0b287cb1..7839f76f 100644 --- a/packages/size-limit/test/get-config.test.js +++ b/packages/size-limit/test/get-config.test.js @@ -429,6 +429,48 @@ it('normalizes import', async () => { }) }) +it('normalizes networkSpeed option for time plugin', async () => { + let cwd = 'time-network-speed' + expect(await check(cwd)).toEqual({ + checks: [ + { + files: [fixture(cwd, 'index.js')], + name: 'index.js', + path: 'index.js', + time: { networkSpeed: 20 } + }, + { + files: [fixture(cwd, 'index.js')], + name: 'index.js', + time: { networkSpeed: 20000 } + } + ], + configPath: 'package.json', + cwd: fixture(cwd) + }) +}) + +it('normalizes latency option for time plugin', async () => { + let cwd = 'time-latency' + expect(await check(cwd)).toEqual({ + checks: [ + { + files: [fixture(cwd, 'index.js')], + name: 'index.js', + path: 'index.js', + time: { latency: 0.2 } + }, + { + files: [fixture(cwd, 'index.js')], + name: 'index.js', + time: { latency: 2.35 } + } + ], + configPath: 'package.json', + cwd: fixture(cwd) + }) +}) + const allConfigFileExtensions = ['mjs', 'js', 'cjs', 'ts', 'mts', 'cts'] const exportTypes = [ { exportSyntax: 'export default', moduleType: 'esm' }, diff --git a/packages/time/README.md b/packages/time/README.md index 0324efe4..f9b5a092 100644 --- a/packages/time/README.md +++ b/packages/time/README.md @@ -3,7 +3,46 @@ The plugin for [Size Limit] to track JS download and execution time by [estimo] and Puppeter. -See Size Limit docs for more details. +## Customize Network Speed + +By default, Size Limit measures the loading time of your files using a slow 3G +network (50 kB/s) without latency. You can customize these settings for each +check by modifying your Size Limit configuration: + +1. Install the preset: + +```sh +npm install --save-dev size-limit @size-limit/file @size-limit/time +``` + +2. Add the size-limit [config](https://github.com/ai/size-limit?tab=readme-ov-file#limits-config): + +```js +// .size-limit.js +export default [ + { + path: 'index.js', + time: { + networkSpeed: '5 MB', // Custom network speed for loading files + latency: '800 ms', // Custom network latency + loadingMessage: 'on fast 4G' // Custom message in output + } + } +] +``` + +3. After configuring, run Size Limit to check the customized loading time: + +```sh + $ npm run size-limit + + Package size: 998.6 kB + Loading time: 200 ms on fast 4G + Running time: 214 ms on Snapdragon 410 + Total time: 1.3 s +``` + +See [Size Limit] docs for more details. [Size Limit]: https://github.com/ai/size-limit/ [estimo]: https://github.com/mbalabash/estimo diff --git a/packages/time/index.js b/packages/time/index.js index fe91dfed..e1e3ff29 100644 --- a/packages/time/index.js +++ b/packages/time/index.js @@ -8,9 +8,9 @@ async function sum(array, fn) { return (await Promise.all(array.map(fn))).reduce((all, i) => all + i, 0) } -function getLoadingTime(size) { +function getLoadingTime(size, networkSpeed) { if (size === 0) return 0 - let time = size / SLOW_3G + let time = size / networkSpeed if (time < 0.01) time = 0.01 return time } @@ -22,13 +22,15 @@ export default [ if (typeof check.size === 'undefined') { throw new SizeLimitError('missedPlugin', 'file') } - check.loadTime = getLoadingTime(check.size) + let networkSpeed = (check.time && check.time.networkSpeed) || SLOW_3G + let latency = (check.time && check.time.latency) || 0 + check.loadTime = getLoadingTime(check.size, networkSpeed) + latency if (check.running !== false) { let files = check.bundles || check.files check.runTime = await sum(files, i => getRunningTime(i)) - check.time = check.runTime + check.loadTime + check.totalTime = check.runTime + check.loadTime } else { - check.time = check.loadTime + check.totalTime = check.loadTime } }, wait80: 'Running JS in headless Chrome' diff --git a/packages/time/test/index.test.js b/packages/time/test/index.test.js index f159b555..65e991f1 100644 --- a/packages/time/test/index.test.js +++ b/packages/time/test/index.test.js @@ -33,7 +33,7 @@ it('calculates time to download and run', async () => { loadTime: 20.48, runTime: 10, size: 1024 * 1024, - time: 30.48 + totalTime: 30.48 }, { files: ['/b'], size: 1024 * 1024 } ] @@ -53,7 +53,7 @@ it('avoids run on request', async () => { loadTime: 20.48, running: false, size: 1024 * 1024, - time: 20.48 + totalTime: 20.48 }) }) @@ -94,3 +94,50 @@ it('throws an error on missed size', async () => { } expect(err).toEqual(new SizeLimitError('missedPlugin', 'file')) }) + +it('uses provided network speed for calculating loading time', async () => { + let config = { + checks: [ + { running: false, size: 1024 * 1024, time: { networkSpeed: 100 * 1024 } } + ] + } + + await time.step80(config, config.checks[0]) + expect(config.checks[0].loadTime).toBe( + config.checks[0].size / config.checks[0].time.networkSpeed + ) +}) + +it('uses provided latency to loading time', async () => { + let size = 1024 * 1024 + let config = { + checks: [ + { running: false, size }, + { running: false, size, time: { latency: 8 } } + ] + } + + await time.step80(config, config.checks[0]) + await time.step80(config, config.checks[1]) + expect(config.checks[1].loadTime).toBe( + config.checks[0].loadTime + config.checks[1].time.latency + ) +}) + +it('uses provided network speed and latency for calculating loading time', async () => { + let config = { + checks: [ + { + running: false, + size: 1024 * 1024, + time: { latency: 0.1984, networkSpeed: 100 * 1024 } + } + ] + } + + await time.step80(config, config.checks[0]) + expect(config.checks[0].loadTime).toBe( + config.checks[0].size / config.checks[0].time.networkSpeed + + config.checks[0].time.latency + ) +})