├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── LICENSE ├── README.md ├── demo.gif ├── lib ├── chrome.js ├── cli.js ├── client.js ├── format-benchmark.js ├── index.d.ts ├── index.js ├── server.js └── webpack.js ├── package.json ├── test ├── cli.js ├── fixtures │ ├── .babelrc │ ├── benchmark.js │ ├── benchmark.jsx │ ├── benchmark.tsx │ └── test.jsx ├── format-benchmark.js └── index.js ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14, 16, 18] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Cache dependencies 22 | uses: actions/cache@v1 23 | with: 24 | path: node_modules 25 | key: node${{ matrix.node-version }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 26 | 27 | - name: Install dependencies 28 | run: yarn --frozen-lockfile 29 | 30 | - name: Lint files and run tests 31 | run: yarn test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright © 2018, Roland Warmerdam 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-benchmark 2 | 3 | ![Demo](demo.gif) 4 | 5 | A tool for benchmarking the render performance of React components. 6 | 7 | It compiles the benchmark code into a minified production bundle using Webpack and then runs it in headless Chrome to benchmark the real production code in a real production environment. 8 | 9 | _Note: the benchmark numbers aren՚t completely accurate and should only be used relatively to compare the performance difference of code changes or different implementations._ 10 | 11 | ## Install 12 | 13 | ```sh 14 | yarn global add react-benchmark 15 | # or 16 | npm install -g react-benchmark 17 | ``` 18 | 19 | ## Usage 20 | 21 | ``` 22 | Usage 23 | $ react-benchmark 24 | 25 | Options 26 | Path to a JavaScript or TypeScript file that exports the function to be benchmarked. 27 | --debug, -d Run a development build instead of a production build to aid debugging. 28 | --devtools, -t Run Chrome in windowed mode with the devtools open. 29 | --cpuThrottle=X Run Chrome with CPU throttled X times. 30 | --version Prints the version. 31 | --help Prints this message. 32 | 33 | Examples 34 | $ react-benchmark benchmark.js 35 | ``` 36 | 37 | The `` file should export a function that returns the component instance you want to benchmark. For example: 38 | 39 | ```js 40 | import React from 'react' 41 | import Component from './src' 42 | 43 | export default function () { 44 | return 45 | } 46 | ``` 47 | 48 | You can import anything that Webpack supports out of the box and your code will be transpiled with Babel using your local Babel config. TypeScript files are also supported out of the box. 49 | 50 | ## API 51 | 52 | `react-benchmark` exports a `ReactBenchmark` class that instantiates an event emitter with a `.run()` method. Calling the `.run()` method will start the benchmark with the provided options. 53 | 54 | ```js 55 | const ReactBenchmark = require('react-benchmark') 56 | const reactBenchmark = new ReactBenchmark() 57 | 58 | reactBenchmark.on('progress', (currentStats) => { 59 | console.log(currentStats) 60 | }) 61 | 62 | const result = await reactBenchmark.run('benchmark.js') 63 | ``` 64 | 65 | See the [CLI code](lib/cli.js) for a full implementation example. 66 | 67 | ### .run(filepath, options) 68 | 69 | Starts the benchmark. Returns a Promise that will resolve to a [Benchmark](https://benchmarkjs.com/docs) object containing the stats once the benchmark has been completed. 70 | 71 | #### filepath 72 | 73 | Type: `String` 74 | 75 | Path to the benchmark file to run. See the [Usage](#usage) section for more details. 76 | 77 | #### options 78 | 79 | Type: `Object` 80 | Default: `{ debug: false, devtools: false, cpuThrottle: 1 }` 81 | 82 | Optional object containing additional options. 83 | 84 | ##### debug 85 | 86 | Type: `Boolean`
87 | Default: `false` 88 | 89 | Run a development build instead of a production build to aid debugging. 90 | 91 | ##### devtools 92 | 93 | Type: `Boolean`
94 | Default: `false` 95 | 96 | Run Chrome in windowed mode with the devtools open. 97 | 98 | ##### cpuThrottle 99 | 100 | Type: `number`
101 | Default: `1` 102 | 103 | Run Chrome with CPU throttled X times. Useful to receive more precise results between runs. 104 | 105 | ### Events 106 | 107 | #### webpack 108 | 109 | Fired when the Webpack build has started. 110 | 111 | #### server 112 | 113 | Fired when the webserver has started. 114 | 115 | #### chrome 116 | 117 | Fired when Chrome has launched. 118 | 119 | #### start 120 | 121 | Fired when the actual benchmark starts. 122 | 123 | #### progress 124 | 125 | Fired every time a benchmark cycle has been completed. Gets passed a [Benchmark](https://benchmarkjs.com/docs) object with the current stats. This event will be fired multiple times per run. 126 | 127 | #### console 128 | 129 | Fired every time something is logged to Chrome՚s console. Gets passed a `{type, text}` object. 130 | 131 | ## License 132 | 133 | react-benchmark is released under the ISC license. 134 | 135 | Copyright © 2018, Roland Warmerdam. 136 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rowno/react-benchmark/b9cc34860f1b4730b2bfe77cae2cb174379340e0/demo.gif -------------------------------------------------------------------------------- /lib/chrome.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const EventEmitter = require('events') 3 | const puppeteer = require('puppeteer') 4 | 5 | module.exports = class Chrome extends EventEmitter { 6 | constructor() { 7 | super() 8 | 9 | this.chrome = null 10 | } 11 | 12 | async start(port, devtools, { cpuThrottle }) { 13 | let completed = false 14 | 15 | this.chrome = await puppeteer.launch({ devtools }) 16 | const page = await this.chrome.newPage() 17 | const client = await page.target().createCDPSession() 18 | 19 | await client.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottle }) 20 | 21 | this.chrome.on('disconnected', () => { 22 | this.chrome = null 23 | 24 | if (completed) { 25 | this.emit('close') 26 | } else { 27 | this.emit('error', new Error('Chrome disconnected unexpectedly')) 28 | } 29 | }) 30 | this.chrome.on('targetdestroyed', async (target) => { 31 | try { 32 | if ((await target.page()) === page) { 33 | if (completed) { 34 | this.emit('close') 35 | } else { 36 | this.emit('error', new Error('Chrome tab closed unexpectedly')) 37 | } 38 | } 39 | } catch (error) { 40 | // Workaround target.page() throwing an error when Chrome is closing 41 | if ( 42 | !error.message.includes('No target with given id found undefined') 43 | ) { 44 | this.emit('error', error) 45 | } 46 | } 47 | }) 48 | 49 | page.on('console', (msg) => { 50 | this.emit('console', { type: msg.type(), text: msg.text() }) 51 | }) 52 | page.on('pageerror', (err) => { 53 | this.emit('error', err) 54 | }) 55 | page.on('requestfailed', (request) => { 56 | const error = new Error(`${request.failure().errorText} ${request.url()}`) 57 | this.emit('error', error) 58 | }) 59 | 60 | page.exposeFunction('benchmarkProgress', (data) => { 61 | const benchmark = JSON.parse(data) 62 | this.emit('progress', benchmark) 63 | }) 64 | 65 | page.exposeFunction('benchmarkComplete', (data) => { 66 | const benchmark = JSON.parse(data) 67 | completed = true 68 | this.emit('complete', benchmark) 69 | }) 70 | 71 | this.emit('start') 72 | 73 | await page.goto(`http://localhost:${port}`) 74 | } 75 | 76 | async stop() { 77 | if (this.chrome) { 78 | await this.chrome.close() 79 | this.chrome = null 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | require('source-map-support').install() 4 | const ora = require('ora') 5 | const meow = require('meow') 6 | const formatBenchmark = require('./format-benchmark') 7 | const ReactBenchmark = require('.') 8 | 9 | const cli = meow({ 10 | help: ` 11 | Usage 12 | $ react-benchmark 13 | 14 | Options 15 | Path to a JavaScript or TypeScript file that exports the function to be benchmarked. 16 | --debug, -d Run a development build instead of a production build to aid debugging. 17 | --devtools, -t Run Chrome in windowed mode with the devtools open. 18 | --cpuThrottle=X Run Chrome with CPU throttled X times. 19 | --version Prints the version. 20 | --help Prints this message. 21 | 22 | Examples 23 | $ react-benchmark benchmark.js 24 | `.trim(), 25 | flags: { 26 | debug: { 27 | type: 'boolean', 28 | default: false, 29 | alias: 'd', 30 | }, 31 | devtools: { 32 | type: 'boolean', 33 | default: false, 34 | alias: 't', 35 | }, 36 | cpuThrottle: { 37 | type: 'number', 38 | default: 1, 39 | }, 40 | }, 41 | }) 42 | 43 | let spinner 44 | 45 | async function main() { 46 | if (cli.input.length !== 1) { 47 | cli.showHelp() 48 | return 49 | } 50 | 51 | const [filepath] = cli.input 52 | const { debug, devtools, cpuThrottle } = cli.flags 53 | 54 | spinner = ora().start() 55 | 56 | const reactBenchmark = new ReactBenchmark() 57 | 58 | reactBenchmark.on('webpack', () => { 59 | // Add trailing spaces so that if ts-loader console.log's something it's easier to read 60 | spinner.text = 'Compiling bundle ' 61 | }) 62 | 63 | reactBenchmark.on('server', () => { 64 | spinner.text = 'Starting server ' 65 | }) 66 | 67 | reactBenchmark.on('chrome', () => { 68 | spinner.text = 'Starting Chrome ' 69 | }) 70 | 71 | reactBenchmark.on('start', () => { 72 | spinner.text = 'Starting benchmark ' 73 | }) 74 | 75 | reactBenchmark.on('progress', (benchmark) => { 76 | spinner.text = formatBenchmark(benchmark) 77 | }) 78 | 79 | reactBenchmark.on('console', (log) => { 80 | spinner.clear() 81 | // Log to stderr so that stdout only contains the final output 82 | console.error(`console.${log.type}: ${log.text}`) 83 | spinner.render() 84 | }) 85 | 86 | const result = await reactBenchmark.run(filepath, { 87 | debug, 88 | devtools, 89 | cpuThrottle, 90 | }) 91 | 92 | spinner.stop() 93 | console.log(formatBenchmark(result)) 94 | } 95 | 96 | main().catch((error) => { 97 | if (spinner) { 98 | spinner.fail() 99 | } 100 | 101 | console.error(error) 102 | process.exitCode = 1 103 | }) 104 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | import benchmark from 'benchmark' 2 | import lodash from 'lodash' 3 | import ReactDOM from 'react-dom' 4 | import testComponent from 'react-benchmark-test-component' 5 | 6 | // Hack to make benchmark work via webpack 7 | const Benchmark = benchmark.runInContext({ _: lodash }) 8 | window.Benchmark = Benchmark 9 | 10 | // Render an instance in the DOM before running the benchmark to make debugging easier 11 | const container = document.createElement('div') 12 | ReactDOM.render(testComponent(), container) 13 | document.body.append(container) 14 | 15 | const bench = new Benchmark({ 16 | defer: true, 17 | async: true, 18 | fn(deferred) { 19 | const container = document.createElement('div') 20 | ReactDOM.render(testComponent(), container, () => { 21 | deferred.resolve() 22 | }) 23 | }, 24 | onCycle(e) { 25 | window.benchmarkProgress(JSON.stringify(e.target)) 26 | }, 27 | onComplete() { 28 | window.benchmarkComplete(JSON.stringify(bench)) 29 | }, 30 | }) 31 | 32 | bench.run() 33 | -------------------------------------------------------------------------------- /lib/format-benchmark.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const humanizeNumber = require('humanize-number') 3 | const pluralize = require('pluralize') 4 | 5 | module.exports = (benchmark) => { 6 | const ops = benchmark.hz // Can be null on the first run if it executes really quickly 7 | ? humanizeNumber(benchmark.hz.toFixed(benchmark.hz < 100 ? 2 : 0)) 8 | : 0 9 | const marginOfError = benchmark.stats.rme.toFixed(2) 10 | const runs = pluralize('run', benchmark.stats.sample.length, true) 11 | return `${ops} ops/sec ±${marginOfError}% (${runs} sampled)` 12 | } 13 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import Benchmark from 'benchmark' 2 | import EventEmitter from 'events' 3 | 4 | export interface Log { 5 | type: string 6 | text: string 7 | } 8 | 9 | export interface RunOptions { 10 | /** 11 | * Run a development build instead of a production build to aid debugging. 12 | * @default false 13 | */ 14 | debug?: boolean 15 | /** 16 | * Run Chrome in windowed mode with the devtools open. 17 | * @default false 18 | */ 19 | devtools?: boolean 20 | /** 21 | * Run Chrome with CPU throttled X times. Useful to receive more precise results between runs. 22 | * @default 1 23 | */ 24 | cpuThrottle?: number 25 | } 26 | 27 | export default class ReactBenchmark extends EventEmitter { 28 | /** 29 | * Starts the benchmark. 30 | * @param filepath Path to the benchmark file to run. 31 | * @param options Optional object containing additional options. 32 | * @returns A Promise that will resolve to a [Benchmark](https://benchmarkjs.com/docs) object containing the stats once the benchmark has been completed. 33 | */ 34 | run(filepath: string, options?: RunOptions): Promise 35 | 36 | /** Fired when the Webpack build has started. */ 37 | on(event: 'webpack', callback: () => void): void 38 | /** Fired when the webserver has started. */ 39 | on(event: 'server', callback: () => void): void 40 | /** Fired when Chrome has launched. */ 41 | on(event: 'chrome', callback: () => void): void 42 | /** Fired when the actual benchmark starts. */ 43 | on(event: 'start', callback: () => void): void 44 | /** Fired every time a benchmark cycle has been completed. */ 45 | on(event: 'progress', callback: (benchmark: Benchmark) => void): void 46 | /** Fired every time something is logged to Chrome՚s console. */ 47 | on(event: 'console', callback: (log: Log) => void): void 48 | } 49 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const EventEmitter = require('events') 4 | const tempy = require('tempy') 5 | const fs = require('fs-extra') 6 | const webpack = require('./webpack') 7 | const Chrome = require('./chrome') 8 | const Server = require('./server') 9 | 10 | module.exports = class ReactBenchmark extends EventEmitter { 11 | constructor() { 12 | super() 13 | 14 | this.chrome = new Chrome() 15 | this.server = new Server() 16 | 17 | this.chrome.on('start', () => { 18 | this.emit('start') 19 | }) 20 | this.chrome.on('progress', (benchmark) => { 21 | this.emit('progress', benchmark) 22 | }) 23 | this.chrome.on('console', (log) => { 24 | this.emit('console', log) 25 | }) 26 | this.chrome.on('close', () => { 27 | this._shutdown() 28 | }) 29 | } 30 | 31 | async _shutdown() { 32 | await this.chrome.stop() 33 | await this.server.stop() 34 | this.running = false 35 | } 36 | 37 | async run( 38 | filepath, 39 | { debug = false, devtools = false, cpuThrottle = 1 } = {} 40 | ) { 41 | if (this.running) { 42 | throw new Error('Benchmark is already running') 43 | } 44 | 45 | this.running = true 46 | 47 | const benchmarkPath = path.resolve(filepath) 48 | 49 | if (!(await fs.pathExists(benchmarkPath))) { 50 | throw new Error('Benchmark file doesn՚t exist') 51 | } 52 | 53 | const outputPath = tempy.directory() 54 | 55 | this.emit('webpack') 56 | 57 | await webpack.compile(outputPath, benchmarkPath, debug) 58 | 59 | this.emit('server') 60 | 61 | const port = await this.server.start(outputPath) 62 | 63 | return new Promise((resolve, reject) => { 64 | this.chrome.once('complete', async (benchmark) => { 65 | if (!devtools) { 66 | await this._shutdown() 67 | } 68 | resolve(benchmark) 69 | }) 70 | 71 | this.chrome.once('error', async (err) => { 72 | if (!devtools) { 73 | await this._shutdown() 74 | } 75 | reject(err) 76 | }) 77 | 78 | this.emit('chrome') 79 | 80 | this.chrome.start(port, devtools, { cpuThrottle }) 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const express = require('express') 3 | const getPort = require('get-port') 4 | 5 | module.exports = class Server { 6 | constructor() { 7 | this.server = null 8 | } 9 | 10 | async start(assetsPath) { 11 | const port = await getPort() 12 | const app = express() 13 | 14 | app.use(express.static(assetsPath)) 15 | 16 | return new Promise((resolve) => { 17 | this.server = app.listen(port, () => resolve(port)) 18 | }) 19 | } 20 | 21 | stop() { 22 | return new Promise((resolve) => { 23 | if (this.server) { 24 | this.server.close(() => { 25 | this.server = null 26 | resolve() 27 | }) 28 | } else { 29 | resolve() 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/webpack.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const os = require('os') 4 | const webpack = require('webpack') 5 | const HtmlWebpackPlugin = require('html-webpack-plugin') 6 | const resolveFrom = require('resolve-from') 7 | const pkgDir = require('pkg-dir') 8 | 9 | function resolvePackage(benchmarkPath, packageName) { 10 | // Try resolving the package relative to the benchmark file and 11 | // fallback to the react-benchmark's node_modules 12 | return ( 13 | resolveFrom.silent(benchmarkPath, `${packageName}/package.json`) || 14 | require.resolve(`${packageName}/package.json`) 15 | ).replace(/package\.json$/, '') // Get package's directory rather than it's filepath so webpack can do it's magic 16 | } 17 | 18 | async function resolveProjectRoot(benchmarkPath) { 19 | // Try resolving the project root from the benchmark file and fallback to the 20 | // user's home directory 21 | const projectRoot = await pkgDir(path.dirname(benchmarkPath)) 22 | return projectRoot || os.homedir() 23 | } 24 | 25 | exports.compile = async (outputPath, benchmarkPath, debug) => { 26 | // Guess the project root directory so that babel can resolve it's config correctly 27 | const projectRoot = await resolveProjectRoot(benchmarkPath) 28 | 29 | const babelLoader = { 30 | loader: 'babel-loader', 31 | options: { 32 | cwd: projectRoot, 33 | }, 34 | } 35 | 36 | return new Promise((resolve, reject) => { 37 | /** @type {import('webpack').Configuration} */ 38 | const config = { 39 | mode: debug ? 'development' : 'production', 40 | context: __dirname, 41 | amd: false, 42 | resolve: { 43 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 44 | alias: { 45 | 'react-benchmark-test-component': benchmarkPath, 46 | // Prevent duplicate react's from being bundled 47 | react: resolvePackage(benchmarkPath, 'react'), 48 | 'react-dom': resolvePackage(benchmarkPath, 'react-dom'), 49 | }, 50 | }, 51 | entry: { 52 | bundle: path.join(__dirname, 'client.js'), 53 | }, 54 | output: { 55 | path: outputPath, 56 | }, 57 | plugins: [new HtmlWebpackPlugin()], 58 | performance: { 59 | hints: false, 60 | }, 61 | module: { 62 | noParse: [/node_modules\/benchmark\//], // Parsing benchmark causes it to break 63 | rules: [ 64 | { 65 | test: /\.jsx?$/, 66 | exclude: (path) => 67 | path.includes('node_modules') && 68 | !path.includes('/react-benchmark/lib/'), // Don't exclude ourselves 😆 69 | use: [babelLoader], 70 | }, 71 | { 72 | test: /\.tsx?$/, 73 | use: [ 74 | babelLoader, 75 | { 76 | loader: 'ts-loader', 77 | options: { 78 | compiler: resolvePackage(benchmarkPath, 'typescript'), 79 | transpileOnly: true, 80 | onlyCompileBundledFiles: true, 81 | logLevel: 'error', 82 | }, 83 | }, 84 | ], 85 | }, 86 | ], 87 | }, 88 | } 89 | 90 | if (debug) { 91 | // Load existing source maps (from node_modules etc) 92 | config.module.rules.push({ 93 | test: /\.[jt]sx?$/, 94 | loader: 'source-map-loader', 95 | enforce: 'pre', 96 | }) 97 | config.devtool = 'inline-cheap-module-source-map' 98 | } 99 | 100 | webpack(config, (err, stats) => { 101 | if (err || stats.hasErrors()) { 102 | return reject(err || stats.toJson().errors[0]) 103 | } 104 | 105 | resolve() 106 | }) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-benchmark", 3 | "version": "5.1.0", 4 | "description": "A tool for benchmarking the render performance of React components.", 5 | "author": "Roland Warmerdam (https://roland.codes)", 6 | "keywords": [ 7 | "react", 8 | "benchmark", 9 | "chrome", 10 | "webpack", 11 | "production" 12 | ], 13 | "repository": "Rowno/react-benchmark", 14 | "license": "ISC", 15 | "main": "./lib/index.js", 16 | "types": "./lib/index.d.ts", 17 | "bin": { 18 | "react-benchmark": "./lib/cli.js" 19 | }, 20 | "files": [ 21 | "lib" 22 | ], 23 | "scripts": { 24 | "test": "yarn lint && ava", 25 | "lint": "eslint '**/*.{js,jsx}'", 26 | "prepare": "husky install" 27 | }, 28 | "engines": { 29 | "node": ">=14" 30 | }, 31 | "dependencies": { 32 | "@babel/core": "^7.13.8", 33 | "@types/benchmark": "^2.1.0", 34 | "@types/node": "^14.14.31", 35 | "babel-loader": "^8.2.2", 36 | "benchmark": "^2.1.4", 37 | "express": "^4.17.1", 38 | "fs-extra": "^10.0.0", 39 | "get-port": "^5.0.0", 40 | "html-webpack-plugin": "^5.2.0", 41 | "humanize-number": "^0.0.2", 42 | "lodash": "^4.17.11", 43 | "meow": "^9.0.0", 44 | "ora": "^5.1.0", 45 | "pkg-dir": "^5.0.0", 46 | "pluralize": "^8.0.0", 47 | "puppeteer": "^13.0.1", 48 | "react": "^17.0.1", 49 | "react-dom": "^17.0.1", 50 | "resolve-from": "^5.0.0", 51 | "source-map-loader": "^3.0.1", 52 | "source-map-support": "^0.5.19", 53 | "tempy": "^1.0.0", 54 | "ts-loader": "^9.2.6", 55 | "typescript": "^4.0.3", 56 | "webpack": "^5.24.2" 57 | }, 58 | "devDependencies": { 59 | "@babel/eslint-parser": "^7.16.5", 60 | "@babel/preset-env": "^7.13.9", 61 | "@babel/preset-react": "^7.12.13", 62 | "@types/react": "^17.0.2", 63 | "ava": "^4.3.1", 64 | "eslint": "^8.7.0", 65 | "eslint-config-prettier": "^8.1.0", 66 | "eslint-plugin-react": "^7.22.0", 67 | "execa": "^5.0.0", 68 | "husky": "^8.0.1", 69 | "lint-staged": "^13.0.3", 70 | "prettier": "^2.2.1" 71 | }, 72 | "eslintConfig": { 73 | "parser": "@babel/eslint-parser", 74 | "extends": [ 75 | "eslint:recommended", 76 | "plugin:react/recommended", 77 | "prettier" 78 | ], 79 | "env": { 80 | "es2017": true, 81 | "node": true 82 | }, 83 | "parserOptions": { 84 | "ecmaVersion": 2018, 85 | "requireConfigFile": false 86 | }, 87 | "overrides": [ 88 | { 89 | "files": [ 90 | "lib/client.js" 91 | ], 92 | "env": { 93 | "browser": true, 94 | "node": false 95 | } 96 | } 97 | ], 98 | "settings": { 99 | "react": { 100 | "version": "detect" 101 | } 102 | } 103 | }, 104 | "prettier": { 105 | "semi": false, 106 | "singleQuote": true 107 | }, 108 | "ava": { 109 | "timeout": "20s" 110 | }, 111 | "lint-staged": { 112 | "*.js": [ 113 | "eslint --fix", 114 | "prettier --write" 115 | ] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const test = require('ava') 3 | const execa = require('execa') 4 | 5 | test('runs benchmark', async (t) => { 6 | const binPath = path.resolve(__dirname, '../lib/cli.js') 7 | const fixturePath = path.resolve(__dirname, 'fixtures/benchmark.js') 8 | 9 | const result = await execa(binPath, [fixturePath]) 10 | 11 | t.regex(result.stdout, /[0-9,]+ ops\/sec ±[0-9.]+% \(\d+ runs sampled\)/) 12 | }) 13 | 14 | test('throttles CPU', async (t) => { 15 | const getOpsSec = (resultString) => { 16 | return parseInt( 17 | resultString.match(/([\d,]+) ops\/sec/)[1].replace(/,/g, '') 18 | ) 19 | } 20 | const binPath = path.resolve(__dirname, '../lib/cli.js') 21 | const fixturePath = path.resolve(__dirname, 'fixtures/benchmark.js') 22 | 23 | const woutT = (await execa(binPath, [fixturePath])).stdout 24 | const withT = (await execa(binPath, [fixturePath, '--cpuThrottle=4'])).stdout 25 | 26 | t.assert( 27 | getOpsSec(withT) < getOpsSec(woutT), 28 | 'The difference between throttled and not throttled execution is less then normal' 29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /test/fixtures/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react"] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/benchmark.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | console.warn('log message') 4 | 5 | export default function Benchmark() { 6 | return
Test
7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/benchmark.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Test from './test' 3 | 4 | console.warn('log message') 5 | 6 | export default function Benchmark() { 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/benchmark.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | console.warn('log message') 4 | 5 | const Benchmark: React.FC = () => { 6 | return
Test
7 | } 8 | 9 | export default Benchmark 10 | -------------------------------------------------------------------------------- /test/fixtures/test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Test() { 4 | return
Test
5 | } 6 | -------------------------------------------------------------------------------- /test/format-benchmark.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const formatBenchmark = require('../lib/format-benchmark') 3 | 4 | test('formats a benchmark', (t) => { 5 | const fixture = { 6 | stats: { 7 | moe: 0.0000051306487763758425, 8 | rme: 3.1353553332052244, 9 | sem: 0.000002617677947130532, 10 | deviation: 0.000019413219231213135, 11 | mean: 0.00016363851082648617, 12 | sample: [ 13 | 0.00025766871165644174, 0.0002411764705882353, 0.00019154228855721393, 14 | 0.0001837708830548926, 0.00019093078758949882, 0.00016666666666666666, 15 | 0.00016414686825053996, 0.00016846652267818574, 0.00016149068322981365, 16 | 0.00016356107660455487, 17 | ], 18 | variance: 3.768730809191435e-10, 19 | }, 20 | times: { 21 | cycle: 0.08247380945654903, 22 | elapsed: 6.256, 23 | period: 0.00016363851082648617, 24 | timeStamp: 1517610881510, 25 | }, 26 | hz: 6111.030923890211, 27 | } 28 | t.is(formatBenchmark(fixture), '6,111 ops/sec ±3.14% (10 runs sampled)') 29 | }) 30 | 31 | test('handles a null hz', (t) => { 32 | const fixture = { 33 | stats: { 34 | moe: 0.0000051306487763758425, 35 | rme: 3.1353553332052244, 36 | sem: 0.000002617677947130532, 37 | deviation: 0.000019413219231213135, 38 | mean: 0.00016363851082648617, 39 | sample: [ 40 | 0.00025766871165644174, 0.0002411764705882353, 0.00019154228855721393, 41 | 0.0001837708830548926, 0.00019093078758949882, 0.00016666666666666666, 42 | 0.00016414686825053996, 0.00016846652267818574, 0.00016149068322981365, 43 | 0.00016356107660455487, 44 | ], 45 | variance: 3.768730809191435e-10, 46 | }, 47 | times: { 48 | cycle: 0.08247380945654903, 49 | elapsed: 6.256, 50 | period: 0.00016363851082648617, 51 | timeStamp: 1517610881510, 52 | }, 53 | hz: null, 54 | } 55 | t.is(formatBenchmark(fixture), '0 ops/sec ±3.14% (10 runs sampled)') 56 | }) 57 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const ReactBenchmark = require('..') 3 | 4 | test('runs benchmark', async (t) => { 5 | t.plan(10) 6 | 7 | let hasProgressed = false 8 | const reactBenchmark = new ReactBenchmark() 9 | 10 | reactBenchmark.on('webpack', () => { 11 | t.pass('webpack event') 12 | }) 13 | reactBenchmark.on('server', () => { 14 | t.pass('server event') 15 | }) 16 | reactBenchmark.on('chrome', () => { 17 | t.pass('chrome event') 18 | }) 19 | reactBenchmark.on('start', () => { 20 | t.pass('start event') 21 | }) 22 | reactBenchmark.on('progress', (benchmark) => { 23 | if (!hasProgressed) { 24 | hasProgressed = true 25 | t.truthy(benchmark.stats) 26 | t.truthy(benchmark.times) 27 | } 28 | }) 29 | reactBenchmark.on('console', (log) => { 30 | t.deepEqual(log, { 31 | type: 'warning', 32 | text: 'log message', 33 | }) 34 | }) 35 | 36 | const result = await reactBenchmark.run('test/fixtures/benchmark.js') 37 | t.truthy(result.stats) 38 | t.truthy(result.times) 39 | t.truthy(result.hz) 40 | }) 41 | 42 | test('supports jsx', async (t) => { 43 | const reactBenchmark = new ReactBenchmark() 44 | const result = await reactBenchmark.run('test/fixtures/benchmark.jsx') 45 | t.truthy(result.stats) 46 | }) 47 | 48 | test('supports typescript', async (t) => { 49 | const reactBenchmark = new ReactBenchmark() 50 | const result = await reactBenchmark.run('test/fixtures/benchmark.tsx') 51 | t.truthy(result.stats) 52 | }) 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es2018", 5 | "module": "commonjs", 6 | "lib": ["es2018", "dom"], 7 | "jsx": "react", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true 10 | }, 11 | "include": ["test/fixtures/benchmark.tsx"] 12 | } 13 | --------------------------------------------------------------------------------