diff --git a/README.md b/README.md index 747bcf8..3a70cc3 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Options Path to a JavaScript or TypeScript file that exports the function to be benchmarked. --debug, -d Run a development build instead of a production build to aid debugging. --devtools, -t Run Chrome in windowed mode with the devtools open. + --onReady Measure time not until the first render, but until `onReady` callback is not invoked from the component. --cpuThrottle=X Run Chrome with CPU throttled X times. --version Prints the version. --help Prints this message. @@ -77,7 +78,7 @@ Path to the benchmark file to run. See the [Usage](#usage) section for more deta #### options Type: `Object` -Default: `{ debug: false, devtools: false, cpuThrottle: 1 }` +Default: `{ debug: false, devtools: false, cpuThrottle: 1, onReady: false }` Optional object containing additional options. @@ -95,6 +96,24 @@ Default: `false` Run Chrome in windowed mode with the devtools open. +##### onReady + +Type: `boolean`
+Default: `false` + +Measure time not until the first render, but until `onReady` callback is not invoked from the component. Useful when you have something happening inside your component after the initial render. If enabled, a special `onReady` function is passed to the component as a prop, which can be called after the component has done all the initial tasks, for example: + +```tsx +// Your component + +const FooComponent = ({ onReady }) => { + useEffect(() => { + doSomeHeavyCalculations().finally(() => onReady()) + }, []) + return () +} +``` + ##### cpuThrottle Type: `number`
diff --git a/lib/cli.js b/lib/cli.js index 7b952d3..9eacd7f 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -15,6 +15,7 @@ Options Path to a JavaScript or TypeScript file that exports the function to be benchmarked. --debug, -d Run a development build instead of a production build to aid debugging. --devtools, -t Run Chrome in windowed mode with the devtools open. + --onReady Measure time not until the first render, but until onReady callback is not invoked from the component. --cpuThrottle=X Run Chrome with CPU throttled X times. --version Prints the version. --help Prints this message. @@ -33,6 +34,10 @@ Examples default: false, alias: 't', }, + onReady: { + type: 'boolean', + default: false, + }, cpuThrottle: { type: 'number', default: 1, @@ -49,7 +54,7 @@ async function main() { } const [filepath] = cli.input - const { debug, devtools, cpuThrottle } = cli.flags + const { debug, devtools, cpuThrottle, onReady } = cli.flags spinner = ora().start() @@ -87,6 +92,7 @@ async function main() { debug, devtools, cpuThrottle, + onReady, }) spinner.stop() diff --git a/lib/client.js b/lib/client.js index 1098e7f..f2487fe 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,5 +1,6 @@ import benchmark from 'benchmark' import lodash from 'lodash' +import React from 'react' import ReactDOM from 'react-dom' import testComponent from 'react-benchmark-test-component' @@ -7,19 +8,33 @@ import testComponent from 'react-benchmark-test-component' const Benchmark = benchmark.runInContext({ _: lodash }) window.Benchmark = Benchmark -// Render an instance in the DOM before running the benchmark to make debugging easier -const container = document.createElement('div') -ReactDOM.render(testComponent(), container) -document.body.append(container) +let container = document.createElement('div') const bench = new Benchmark({ defer: true, async: true, fn(deferred) { - const container = document.createElement('div') - ReactDOM.render(testComponent(), container, () => { + container.remove() + container = document.createElement('div') + document.body.append(container) + const handleReady = () => { + if (process.env.IS_ON_READY_ENABLED !== 'true') { + return + } deferred.resolve() - }) + } + ReactDOM.render( + React.createElement(testComponent, { + onReady: process.env.IS_ON_READY_ENABLED ? handleReady : undefined, + }), + container, + () => { + if (process.env.IS_ON_READY_ENABLED === 'true') { + return + } + deferred.resolve() + } + ) }, onCycle(e) { window.benchmarkProgress(JSON.stringify(e.target)) diff --git a/lib/index.d.ts b/lib/index.d.ts index 4e6b742..28d4ae3 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -21,7 +21,12 @@ export interface RunOptions { * Run Chrome with CPU throttled X times. Useful to receive more precise results between runs. * @default 1 */ - cpuThrottle?: number + cpuThrottle?: number, + /** + * Measure time not until the first render, but until onReady callback is not invoked from the component. + * @default false + */ + onReady?: boolean, } export default class ReactBenchmark extends EventEmitter { diff --git a/lib/index.js b/lib/index.js index fca496a..4f2e2b3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -36,8 +36,9 @@ module.exports = class ReactBenchmark extends EventEmitter { async run( filepath, - { debug = false, devtools = false, cpuThrottle = 1 } = {} + { debug = false, devtools = false, cpuThrottle = 1, onReady = false } = {} ) { + process.env.IS_ON_READY_ENABLED = onReady if (this.running) { throw new Error('Benchmark is already running') } diff --git a/lib/webpack.js b/lib/webpack.js index 343bf2a..60349d8 100644 --- a/lib/webpack.js +++ b/lib/webpack.js @@ -54,7 +54,10 @@ exports.compile = async (outputPath, benchmarkPath, debug) => { output: { path: outputPath, }, - plugins: [new HtmlWebpackPlugin()], + plugins: [ + new HtmlWebpackPlugin(), + new webpack.EnvironmentPlugin(['IS_ON_READY_ENABLED']), + ], performance: { hints: false, }, diff --git a/package.json b/package.json index e15e7cc..b2a7696 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,9 @@ "react": { "version": "detect" } + }, + "globals": { + "process": true } }, "prettier": { diff --git a/test/cli.js b/test/cli.js index e46a238..4ac6ad0 100644 --- a/test/cli.js +++ b/test/cli.js @@ -28,3 +28,12 @@ test('throttles CPU', async (t) => { 'The difference between throttled and not throttled execution is less then normal' ) }) + +test('uses onReady cb when --onReady flag is passed', async (t) => { + const binPath = path.resolve(__dirname, '../lib/cli.js') + const fixturePath = path.resolve(__dirname, 'fixtures/benchmark-onready.tsx') + + const result = await execa(binPath, [fixturePath]) + + t.regex(result.stdout, /[0-9,]+ ops\/sec ±[0-9.]+% \(\d+ runs sampled\)/) +}) diff --git a/test/fixtures/benchmark-onready.tsx b/test/fixtures/benchmark-onready.tsx new file mode 100644 index 0000000..14c2597 --- /dev/null +++ b/test/fixtures/benchmark-onready.tsx @@ -0,0 +1,51 @@ +import React, { useState, useEffect } from 'react' + +type BenchmarkProps = { + onReady: () => void +} + +const BenchmarkOnReady: React.FC = ({ onReady }) => { + if (!onReady) { + throw new Error('onReady has not been passed from react-benchmark') + } + const NODES_COUNT = 2 + const [nodes, setNodes] = useState([]) + const getNodes = (numberOfNodes: number, page: number) => { + const newNodes: React.ReactNode[] = [] + for (let i = numberOfNodes * (page + 1); i > numberOfNodes * page; i--) { + newNodes.push( +

+ {i} bottles of beer on the wall +
+ {i} bottles of beer! +
+ Take one down, pass it around +
+ {i - 1} bottles of beer on the wall! +

+ ) + } + return newNodes + } + const addDefferedNodes = ( + nodesCount: number, + page: number, + timeout: number + ) => { + setTimeout(() => { + setNodes((n) => [...n, ...getNodes(nodesCount, page)]) + }, timeout) + } + + useEffect(() => { + addDefferedNodes(NODES_COUNT, 1, 1) + }) + useEffect(() => { + if (nodes.length === NODES_COUNT) { + onReady() + } + }, [nodes, onReady]) + return
{nodes}
+} + +export default BenchmarkOnReady