{"id":16429723,"url":"https://github.com/dominique-mueller/react-render-scheduling-analysis","last_synced_at":"2025-02-25T08:17:49.264Z","repository":{"id":66237596,"uuid":"257031311","full_name":"dominique-mueller/react-render-scheduling-analysis","owner":"dominique-mueller","description":"[ANALYSIS] Render scheduling in React","archived":false,"fork":false,"pushed_at":"2021-01-18T15:50:40.000Z","size":6023,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-01-07T22:36:38.764Z","etag":null,"topics":["batch","macrotask","microtask","promises","react","render","rxjs","scheduling"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dominique-mueller.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-19T15:15:23.000Z","updated_at":"2021-12-01T09:57:17.000Z","dependencies_parsed_at":"2023-02-20T16:46:07.067Z","dependency_job_id":null,"html_url":"https://github.com/dominique-mueller/react-render-scheduling-analysis","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominique-mueller%2Freact-render-scheduling-analysis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominique-mueller%2Freact-render-scheduling-analysis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominique-mueller%2Freact-render-scheduling-analysis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominique-mueller%2Freact-render-scheduling-analysis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dominique-mueller","download_url":"https://codeload.github.com/dominique-mueller/react-render-scheduling-analysis/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240627959,"owners_count":19831600,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["batch","macrotask","microtask","promises","react","render","rxjs","scheduling"],"created_at":"2024-10-11T08:24:10.145Z","updated_at":"2025-02-25T08:17:49.201Z","avatar_url":"https://github.com/dominique-mueller.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# react-render-scheduling-analysis\n\nUsing custom scheduling in **[React](https://github.com/facebook/react)**.\n\n\u003c/div\u003e\n\n\u003cbr\u003e\u003cbr\u003e\u003cbr\u003e\n\n## Scheduling in React\n\n### Automatic scheduling in React\n\nWithin scopes known to React - such as a `useEffect()` hook body or an event handler function - all synchronous function calls that\nmanipulate state (using the `useState()` hook) will be batched up automatically, thus leading to only a single scheduled re-render instead\nof potentially multiple ones. [[1](https://reactjs.org/docs/faq-state.html#when-is-setstate-asynchronous),\n[2](https://overreacted.io/react-as-a-ui-runtime/),\n[3](https://github.com/facebook/react/issues/16387#issuecomment-521623662)]\n\nFor instance:\n\n```ts\n// State\nconst [value, setValue] = useState('');\nconst [otherValue, setOtherValue] = useState('');\nconst [thisValue, setThisValue] = useState('');\nconst [thatValue, setThatValue] = useState('');\n\n// Side effects\nuseEffect(() =\u003e {\n  setOtherValue(`other ${value}.`);\n  setThisValue(`this ${value}.`);\n  setThatValue(`that ${value}.`);\n}, [value]);\n```\n\nLet's assume we update the value, e.g. within an event handler. For instance:\n\n```ts\nsetValue('cool value');\n```\n\nNow, the following re-renderings happen:\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003eRender\u003c/th\u003e\n      \u003cth\u003eDescription\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e1\u003c/td\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e\n        Calling \u003ccode\u003esetValue('cool value')\u003c/code\u003e will lead to the \u003ccode\u003evalue\u003c/code\u003e state variable being updated by the\n        \u003ccode\u003euseState()\u003c/code\u003e hook. Because updating state via the \u003ccode\u003euseState()\u003c/code\u003e hook will always trigger a re-render - at least\n        if the value has actually changed, which in our case it did - React triggers a re-render.\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e2\u003c/td\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e\n        Our \u003ccode\u003euseEffect()\u003c/code\u003e hook lists the \u003ccode\u003evalue\u003c/code\u003e state variable as a dependency. Thus, changing the \u003ccode\u003evalue\u003c/code\u003e\n        state variable value will always lead to the \u003ccode\u003euseEffect()\u003c/code\u003e hook being executed. Within our \u003ccode\u003euseEffect()\u003c/code\u003e hook,\n        we update three other state variables (again, based on `useState()`). Now: React has enough context for optimization: It knows what\n        \u003ccode\u003euseEffect()\u003c/code\u003e is and does, and it has full control over / executes \u003ccode\u003euseEffect()\u003c/code\u003e. Thus, React intelligently\n        batches up all three state changes instead of executing them directly, thus scheduling a single re-render instead of three separate\n        ones.\n      \u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003cbr\u003e\n\n### No automatic scheduling in asynchronous `useEffect()`s / event handlers\n\nNow, things are different when asynchronous operations (e.g. promises, timeouts, RxJS, ...) come into play.\n\nFor instance:\n\n```ts\n// State\nconst [value, setValue] = useState('');\nconst [otherValue, setOtherValue] = useState('');\nconst [thisValue, setThisValue] = useState('');\nconst [thatValue, setThatValue] = useState('');\n\n// Side effects\nuseEffect(() =\u003e {\n  Promise.resolve().then(() =\u003e {\n    setOtherValue(`other ${value}.`);\n    setThisValue(`this ${value}.`);\n    setThatValue(`that ${value}.`);\n  });\n}, [value]);\n```\n\nLet's assume we update the value, e.g. within an event handler. For instance:\n\n```ts\nsetValue('cool value');\n```\n\nNow, the following re-renderings happen:\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003eRender\u003c/th\u003e\n      \u003cth\u003eDescription\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e1\u003c/td\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e\n        Calling \u003ccode\u003esetValue('cool value')\u003c/code\u003e will lead to the \u003ccode\u003evalue\u003c/code\u003e state variable being updated by the\n        \u003ccode\u003euseState()\u003c/code\u003e hook. Because updating state via the \u003ccode\u003euseState()\u003c/code\u003e hook will always trigger a re-render - at least\n        if the value has actually changed, which in our case it did - React triggers a re-render.\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e2, 3, 4\u003c/td\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e\n        Our \u003ccode\u003euseEffect()\u003c/code\u003e hook lists the \u003ccode\u003evalue\u003c/code\u003e state variable as a dependency. Thus, changing the \u003ccode\u003evalue\u003c/code\u003e\n        state variable value will always lead to the \u003ccode\u003euseEffect()\u003c/code\u003e hook being executed. Within our \u003ccode\u003euseEffect()\u003c/code\u003e hook,\n        we update three other state variables (again, based on \u003ccode\u003euseState()\u003c/code\u003e) once our promise resolves. Now: Due to our side\n        effects being executed asynchronously (once the promise resolves), React does no longer have enough context for optimizations. Thus,\n        in order to ensure that nothing breaks, React has no other choice than executing all state changes as per usual, leading to three\n        separate re-renders.\n      \u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003cbr\u003e\n\n### No automatic scheduling across chained `useEffect()`s\n\nAlso: Automatic batching / scheduling only works within each `useEffect()` body, not across multiple `useEffect()`s. If you have side\neffects leading to new state that then triggers side effects - basically a chain of `useEffects()` - React is not able to optimize this as\nit is too unpredicable.\n\nFor instance:\n\n```ts\n// State\nconst [value, setValue] = useState('');\nconst [otherValue, setOtherValue] = useState('');\nconst [thisValue, setThisValue] = useState('');\n\n// Side effects\nuseEffect(() =\u003e {\n  setOtherValue(`other ${value}.`);\n}, [value]);\nuseEffect(() =\u003e {\n  setThisValue(`this ${value}.`);\n}, [otherValue]);\n```\n\nLet's assume we update the value, e.g. within an event handler. For instance:\n\n```ts\nsetValue('cool value');\n```\n\nNow, the following re-renderings happen:\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003eRender\u003c/th\u003e\n      \u003cth\u003eDescription\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e1\u003c/td\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e\n        Calling \u003ccode\u003esetValue('cool value')\u003c/code\u003e will lead to the \u003ccode\u003evalue\u003c/code\u003e state variable being updated by the\n        \u003ccode\u003euseState()\u003c/code\u003e hook. Because updating state via the \u003ccode\u003euseState()\u003c/code\u003e hook will always trigger a re-render - at least\n        if the value has actually changed, which in our case it did - React triggers a re-render.\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e2\u003c/td\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e\n        Our first \u003ccode\u003euseEffect()\u003c/code\u003e hook lists the \u003ccode\u003evalue\u003c/code\u003e state variable as a dependency. Thus, changing the\n        \u003ccode\u003evalue\u003c/code\u003e state variable value will always lead to the \u003ccode\u003euseEffect()\u003c/code\u003e hook being executed. Within our\n        \u003ccode\u003euseEffect()\u003c/code\u003e hook, we update the \u003ccode\u003eotherValue\u003c/code\u003e state variables (again, based on \u003ccode\u003euseState()\u003c/code\u003e). This\n        will lead to a re-render.\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e3\u003c/td\u003e\n      \u003ctd style=\"vertical-align: top\"\u003e\n        Our second \u003ccode\u003euseEffect()\u003c/code\u003e hook has the \u003ccode\u003eotherValue\u003c/code\u003e state variable as its dependency. Thus, the first\n        \u003ccode\u003euseEffect()\u003c/code\u003e changing the \u003ccode\u003eotherValue\u003c/code\u003e state variable value will always lead to the second\n        \u003ccode\u003euseEffect()\u003c/code\u003e hook being executed. Within our  second \u003ccode\u003euseEffect()\u003c/code\u003e hook, we update the other two state\n        variables (again, based on `useState()`). This will, again, lead to a re-render.\n      \u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003cbr\u003e\n\n### No automatic scheduling ouside of React scopes (e.g. RxJS)\n\nWhen managing and distributing state outside of scopes known to React, e.g. when using [RxJS](https://github.com/ReactiveX/rxjs) (instead of\na React Context), React has no way of knowing how to optimize here. Even though the original data source (here our Observable) only emits\ndata once - at the same time, to all components subscribed to it - React will re-render every single component separately.\n\nFor instance:\n\n```tsx\nconst [value, setValue] = useState('');\nconst dataStream = useMyObservable();\n\nuseEffect(() =\u003e {\n  // Update value when it changes\n  const subscription = dataStream.subscribe((newValue) =\u003e {\n    setValue(newValue);\n  });\n\n  // Cleanup\n  return () =\u003e {\n    subscription.unsubscribe();\n  };\n});\n```\n\n\u003cbr\u003e\u003cbr\u003e\u003cbr\u003e\n\n## Manual scheduling in React\n\nReact itself (like many frontend frameworks) hides most of its implementation details and low-level APIs from us, so that we can concentrate\non building applications rather than deep diving into React internals. This also applies to the render pipeline and its scheduling\nmechanisms.\n\n\u003cbr\u003e\n\n### The `unstable_batchedUpdates` API\n\nLuckily for us, though, React actually does expose _one_ API that enables us to group / batch renderings: `unstable_batchedUpdates`.\n\nHowever, this API, like a few others React exposes, is prefixed with `unstable` - meaning it this AP not part of the public\nReact API and thus might change or even break with any future release. But, the `unstable_batchedUpdates` API is the most \"stable unstable\"\nReact APIs out there (see [this tweet](https://twitter.com/dan_abramov/status/1103399900371447819)), and many popular projects rely upon it\n(e.g. [React Redux](https://react-redux.js.org/api/batch)). So it's pretty safe to use.\n\nSo, it's pretty safe to use? For now, yes. Should it get removed at some point (e.g. React 17) we just need to remove the\n`unstable_batchedUpdates` optimzations and - while performance might worsen - our code continues to work just fine. With the upcoming\n[Concurrent Mode](https://reactjs.org/docs/concurrent-mode-intro.html), chances are good that the `unstable_batchedUpdates` API might\nactually become useless anyways as React will be intelligent enough to do most of the optimizations on its own. Until then,\n`unstable_batchedUpdates` is the way to go.\n\n\u003cbr\u003e\n\n### Scheduling synchronously, within a component\n\nIn the rather simple use cases, we can use `unstable_batchedUpdates` right away. A common scenario is running asynchronous code in a\n`useEffect()` hook, and in that situation we can wrap all state change function calls in a single `unstable_batchedUpdates`. This scheduling\nis synchronous, as React will render changes right away instead of sometimes later in the current task.\n\nFor instance:\n\n```diff\n  useEffect(() =\u003e {\n    Promise.resolve().then(() =\u003e {\n+     unstable_batchedUpdates(() =\u003e {\n        setOtherValue(`other ${value}.`);\n        setThisValue(`this ${value}.`);\n        setThatValue(`that ${value}.`);\n+     });\n    });\n  }, [value]);\n```\n\n#### Alternative: Combined state\n\nSometimes, for example if all state lives in the same component, it's just easier to combine multiple `useState()`s into a single\n`useState()`, e.g. by combining state into an object.\n\nFor instance:\n\n```diff\n  useEffect(() =\u003e {\n    Promise.resolve().then(() =\u003e {\n+     unstable_batchedUpdates(() =\u003e {\n-       setOtherValue(`other ${value}.`);\n-       setThisValue(`this ${value}.`);\n-       setThatValue(`that ${value}.`);\n+       setAdditionalValues({\n+         otherValue: `other ${value}.`,\n+         thisValue: `this ${value}.`,\n+         thatValue: `that ${value}.`\n+       });\n+     });\n    });\n  }, [value]);\n```\n\n\u003cbr\u003e\n\n### Scheduling synchronously / asynchronously, across components (render scheduler)\n\nIn more complex situations - e.g. when we want to schedule renderings across multiple components, perhaps even across multiple state\nchanges - we need to be a bit more creative. A custom scheduling solution could exist globally, allowing every component to schedule state\nchanges, and then either we (synchronously) or the browser (asynchronously) will run the state changes wrapped in `unstable_batchedUpdates`.\n\n\u003e The performance analysis below explores those custom scheduling solutions, and if / how they affect performance.\n\n\u003cbr\u003e\u003cbr\u003e\u003cbr\u003e\n\n## Performance Analysis Setup \u0026 Implementation\n\n### Test setup\n\nOf course, we want to get meaningful and consistent results when performance analysis each use case implementation. The following has been\ndone to ensure this:\n\n- **All use case implementations are completely separated from each other.**\n  \u003cbr\u003e\n  Sure, it's a lot of duplicate code and tons of storage used by `node_modules` folders, but it keeps things clean. In particular:\n  - Each use case implementation exists within a separete folder, and no code gets shared between use cases. This way, we can ensure that\n    the implementation is kept to the absolute minimum, e.g. only the relevant scheduler, no unnecessary logic or \"pages\".\n  - Each use case defines and installs its own dependencies, and thus has its own `node_modules` folder. This way, we can easily run our\n    performance analysis tests using different dependencies per use case, e.g. using a newer / experimental version of React.\n- **Performance analysis happens on a production build of the application.**\n  \u003cbr\u003e\n  That's the version our users will see, so that's the version we should test. In particular:\n  - Production builds might perform better (or at least different) than development builds due things like tree shaking, dead code\n    elimination and minification.\n  - React itself actually does additional things in development mode that do affect performance, e.g.\n    [running additional checks or showing development warnings](https://reactjs.org/docs/optimizing-performance.html#use-the-production-build).\n- **Performance analysis happens with the exact same clean browser.**\n  \u003cbr\u003e\n  Let's keep variations to a minimum. In particular:\n  - We use the exact same version of Chrome for all tests, ensuring consistent results.\n  - We use a clean version of Chrome so that things like user profiles, settings or extensions / plugins don't affect the results.\n\nWhile all this certainly helps getting solid test results, there will always be things out of our control, such as:\n\n- Browser stuff (e.g. garbage collection, any internal delays)\n- Software stuff (Windows, software running in the background)\n- Hardware stuff (CPU, GPU, RAM, storage)\n\nAll the performance profiling results documented below ran on the following system:\n\n| Area             | Details                                               |\n| ---------------- | ----------------------------------------------------- |\n| CPU              | Intel Core i7 8700K 6x 3.70Ghz                        |\n| RAM              | 32GB DDR4-3200 DIMM CL16                              |\n| GPU              | NVIDIA GeForce GTX 1070 8GB                           |\n| Storage          | System: 512GB NVMe M.2 SSD, Project: 2TB 7.200rpm HDD |\n| Operating System | Windows 10 Pro, Version 1909, Build 18363.778         |\n\n\u003cbr\u003e\n\n### Test implementation\n\nWithin each test case implementation, the `start-analysis.bin.ts` script is responsible for executing the performance analysis and writing\nthe results onto the disk.\n\nIn particular, it follows these steps:\n\n| Step | Description                                                                 |\n| ---- | --------------------------------------------------------------------------- |\n| 1    | Start the server that serves the frontend application build locally         |\n| 2    | Start the browser, and navigate to the URL serving the frontend application |\n| 3    | Start the browser performance profiler recording                            |\n| -    | Wait for the test to finish                                                 |\n| 4    | Stop the browser performance profiler recording                             |\n| 5    | Write results to disk                                                       |\n| 6    | Close browser                                                               |\n| 7    | Close server                                                                |\n\n\u003e Internally, we use [Puppeteer](https://github.com/puppeteer/puppeteer) to control a browser, and use the native NodeJS server API to serve\n\u003e the fronted to that browser.\n\n\u003cbr\u003e\n\n### How to run a test\n\nTo run a performance analysis on a use case, follow these steps:\n\n1. Install dependencies by running `npm run install`\n2. Create a production build by running `npm run build`\n3. Run the performance analysis by running `npm run start:analysis`\n\nThe script will create the following two files within the `results` folder:\n\n- `profiler-logs.json` contains the React profiler results\n  \u003cbr\u003e\n  The root project is a React app that offers a visualization of this file in the form of charts. Simply run `npm start` and select a\n  `profiler-logs.json` file.\n- `tracing-profile.json` contains the browser performance tracing timeline\n  \u003cbr\u003e\n  This file can be loaded into the \"Performance\" tab of the Chrome Dev Tools, or can be uploaded to and viewed online using the\n  [DevTools Timeline Viewer](https://chromedevtools.github.io/timeline-viewer/)\n\n\u003cbr\u003e\u003cbr\u003e\u003cbr\u003e\n\n## Performance Analysis\n\n### Summary\n\n#### Test parameters\n\nWe are running the performance analysis with the following parameters:\n\n- We render once every second\n- We render 200 components\n- We render 30 times\n\n#### Test results (summary)\n\nThe following table shows a short test summary. See further chapters for more details.\n\n| Test case                                                                                                           | Render time | Comparison (render time) |\n| ------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------ |\n| [No scheduling](#test-case-no-scheduling)                                                                           | ~9.69ms     | 100% (baseline)          |\n| [Synchronous scheduling by manual flush](#test-case-synchronous-scheduling-by-manual-flush)                         | ~1.88ms     | 19.40%                   |\n| [Asynchronous scheduling using Microtasks](#test-case-asynchronous-scheduling-using-microtasks)                     | ~1.81ms     | 18.68%                   |\n| [Asynchronous scheduling using Macrotasks](##test-case-asynchronous-scheduling-using-macrotasks)                    | ~1.84ms     | 18.99%                   |\n| [Asynchronous scheduling using based on render cycle](#test-case-asynchronous-scheduling-based-on-the-render-cycle) | ~1.84ms     | 18.99%                   |\n| [Concurrent Mode (experimental!)](#bonus-concurrent-mode-experimental)                                              | ~1.85ms     | 19.09%                   |\n\nIt should be noted that actual numbers are not that important, mainly because they will never be 100% exact and realisistic, partially also\nbecause the Chrome performance tracing profiler and the React profiler have a hard-to-measure impact on the performance. What's way more\ninteresting here is _how faster or slower_ something got in comparison.\n\n#### Interpretation of results\n\n- Using scheduling (either manually or by switching to Concurrent Mode) generally improves render speeds by a factor of 5.\n- There is no identifyable difference between different render scheduler mechanisms in regards to the render speed. Thus, the decision on\n  which scheduling technique to use just depends on the specific use case. Different scheduling mechanisms may even be combined if useful.\n\n#### Recommendations\n\n- **Concurrent Mode is not yet ready**\n  \u003cbr\u003e\n  Ideally, we want to use features that come with React itself. But as the React Concurrent Mode is still very experimental and requires the\n  App to be stict-mode compatible (which many popular and widely used React libraries are still not yet), it's not a solution for most\n  apps and people (except maybe for people like me who like to live on the edge and break things all the freakin' time).\n- **Synchronous scheduling is doable but not easy**\n  \u003cbr\u003e\n  [Synchronous scheduling by manual flush](#test-case-synchronous-scheduling-by-manual-flush) comes the closest to\n  [not using scheduling](#test-case-no-scheduling), but it requires us to handle the flush by ourselves, probably at the place where state\n  gets managed - seems that this is not the best idea from an architecture point of view?\n- **Asynchronous scheduling is the easiest solution**\n  \u003cbr\u003e\n  [Asynchronous scheduling using Microtasks](#test-case-asynchronous-scheduling-using-microtasks) is probably the easiest and most stable\n  solution. Other asynchronous scheduling techniques may be used depending on the specific use case.\n\n\u003cbr\u003e\n\n### Test case: No scheduling\n\nThis test case shows how performance takes a hit when state is managed and propagated to components outside of scopes known to React, here\nby [RxJS](https://github.com/ReactiveX/rxjs) observables. React will re-render each component separately.\n\nThe test results of this scenario represent the baseline for any further performance improvements.\n\n#### Timeline\n\nThe following chart shows how each state change leads a render step (re-rendering all components) that consists of multiple updates\n(re-rendering one component).\n\n![Profiler Results - Timeline](./docs/results-2020-05-04/default/profiler-logs-timeline.png)\n\n#### Update durations (one component)\n\nA single update (one component re-renders) is very quick (about 0.05ms).\n\n![Profiler Results - Update durations](./docs/results-2020-05-04/default/profiler-logs-update-durations.png)\n\n#### Render durations (all components)\n\nAlthough each single update (one component re-renders) is very quick, running all those updates separetely - with React being unable to\noptimize across those updates - takes time. On average, we are talking about _9.5ms_ to _10ms_, which is way too much when considering a\ncommon frame budget (_16.66ms_).\n\n![Profiler Results - Render durations](./docs/results-2020-05-04/default/profiler-logs-render-durations.png)\n\n#### Tracing\n\nWow, that is one busy tracing profile! We can see lots of fragmentation, the lower parts representing the function calls of React re-rendering components separately.\n\n![Tracing Profile](./docs/results-2020-05-04/default/tracing-profile.png)\n\n\u003cbr\u003e\n\n### Test case: Synchronous scheduling by manual flush\n\nIn this test case, we schedule state changes instead of executing them right away, and then flush them manually (batched up) once all state\nchanges are propagated to components. In particular, we flush all tasks after every observable subscriber has processed the new value.\n\n\u003e Implementation pointers:\n\u003e\n\u003e - [Render scheduler](https://github.com/dominique-mueller/react-scheduling-experiment/blob/master/packages/scheduler-sync/src/RenderScheduler.ts#L6)\n\u003e - [Scheduling tasks](https://github.com/dominique-mueller/react-scheduling-experiment/blob/master/packages/scheduler-sync/src/EventBox.tsx#L29)\n\u003e - [Flushing tasks synchronously](https://github.com/dominique-mueller/react-scheduling-experiment/blob/master/packages/scheduler-sync/src/event-stream.ts#L63)\n\n#### Timeline\n\nThe following chart shows that now each render step (re-rendering all components) contains only one update that now re-renders all\ncomponents at once instead of separately.\n\n![Profiler Results - Timeline](./docs/results-2020-05-04/scheduler-sync/profiler-logs-timeline.png)\n\n#### Update \u0026 render durations (all components)\n\nA single update equals a single render. On average, all component re-render at _1.8ms_ to _1.9ms_ time, which breaks down to a theoratical\ncomponent re-render time of _0.01ms_.\n\n![Profiler Results - Update durations](./docs/results-2020-05-04/scheduler-sync/profiler-logs-render-durations.png)\n\n#### Tracing\n\nThis tracing profile looks very clean, very few function calls compared to the not-scheduled test case.Everything is executed within one synchronous block of code, even scheduled renderings.\n\n![Tracing Profile](./docs/results-2020-05-04/scheduler-sync/tracing-profile.png)\n\n\u003cbr\u003e\n\n### Test case: Asynchronous scheduling using Microtasks\n\nIn this test case, we schedule state changes instead of executing them right away in the form of Microtasks, meaning that the browser will\nautomatically flush all tasks once the currently running synchronous code has been completed and previsouly scheduled Microtasks have been\nexecuted.\n\nIn order to schedule a Microtask, we use the\n[`queueMicrotask()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/queueMicrotask) API:\n\n```ts\nqueueMicrotask(callbackFn);\n```\n\nAs a fallback / polyfill (e.g. for older browsers), we could also use\n[Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve) to schedule a Microtask:\n\n```ts\nPromise.resolve().then(callbackFn);\n```\n\n\u003e Implementation pointers:\n\u003e\n\u003e - [Render scheduler](https://github.com/dominique-mueller/react-scheduling-experiment/blob/master/packages/scheduler-microtask/src/RenderScheduler.ts#L6)\n\u003e - [Scheduling tasks](https://github.com/dominique-mueller/react-scheduling-experiment/blob/master/packages/scheduler-microtask/src/EventBox.tsx#L29)\n\n#### Timeline\n\nThe following chart shows that now each render step (re-rendering all components) contains only one update that now re-renders all\ncomponents at once instead of separately.\n\n![Profiler Results - Timeline](./docs/results-2020-05-04/scheduler-microtask/profiler-logs-timeline.png)\n\n#### Update \u0026 render durations (all components)\n\nA single update equals a single render. On average, all component re-render at _1.8ms_ to _1.9ms_ time, which breaks down to a theoratical\ncomponent re-render time of _0.01ms_.\n\n![Profiler Results - Update durations](./docs/results-2020-05-04/scheduler-microtask/profiler-logs-render-durations.png)\n\n#### Tracing\n\nThis tracing profile looks very clean, very few function calls compared to the not-scheduled test case. Everything is executed within the same Macrotask, the first synchronous block containing the state change and its propagation to the components, the second Microtask block being the execution of all scheduled renderings right after.\n\n![Tracing Profile](./docs/results-2020-05-04/scheduler-microtask/tracing-profile.png)\n\n\u003cbr\u003e\n\n### Test case: Asynchronous scheduling using Macrotasks\n\nIn this test case, we schedule state changes instead of executing them right away in the form of Macrotasks, meaning that the browser will\nautomatically flush all tasks once the currently running synchronous code has been completed and all scheduled Microtasks have been\nexecuted. It's important to note that the browser might render an update on screen before our scheduled code executes.\n\nIn order to schedule a Macrotask, we use the\n[`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) API with a timeout of zero:\n\n```ts\nsetTimeout(callbackFn, 0);\n```\n\n\u003e Implementation pointers:\n\u003e\n\u003e - [Render scheduler](https://github.com/dominique-mueller/react-scheduling-experiment/blob/master/packages/scheduler-macrotask/src/RenderScheduler.ts#L6)\n\u003e - [Scheduling tasks](https://github.com/dominique-mueller/react-scheduling-experiment/blob/master/packages/scheduler-macrotask/src/EventBox.tsx#L29)\n\n#### Timeline\n\nThe following chart shows that now each render step (re-rendering all components) contains only one update that now re-renders all\ncomponents at once instead of separately.\n\n![Profiler Results - Timeline](./docs/results-2020-05-04/scheduler-macrotask/profiler-logs-timeline.png)\n\n#### Update \u0026 render durations (all components)\n\nA single update equals a single render. On average, all component re-render at _1.8ms_ to _1.9ms_ time, which breaks down to a theoratical\ncomponent re-render time of _0.01ms_.\n\n![Profiler Results - Update durations](./docs/results-2020-05-04/scheduler-macrotask/profiler-logs-render-durations.png)\n\n#### Tracing\n\nThis tracing profile looks very clean, very few function calls compared to the not-scheduled test case. The first Macrotask contains the state change and its propagation to the components, the second Macrotask runs all scheduled renderings. We can also clearly see how the browser decided to render a frame in between, which might happen when using Macrotasks for scheduling purposes.\n\n![Tracing Profile](./docs/results-2020-05-04/scheduler-macrotask/tracing-profile.png)\n\n\u003cbr\u003e\n\n### Test case: Asynchronous scheduling based on the render cycle\n\nIn this test case, we schedule state changes instead of executing them right away by deffering them to the render cycle, meaning that the\nbrowser will automatically flush all tasks right before it renders on screen.\n\nIn order to schedule based on the render cycle, we use the\n[`requestAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) API:\n\n```ts\nrequestAnimationFrame(callbackFn);\n```\n\n\u003e Implementation pointers:\n\u003e\n\u003e - [Render scheduler](https://github.com/dominique-mueller/react-scheduling-experiment/blob/master/packages/scheduler-render-cycle/src/RenderScheduler.ts#L6)\n\u003e - [Scheduling tasks](https://github.com/dominique-mueller/react-scheduling-experiment/blob/master/packages/scheduler-render-cycle/src/EventBox.tsx#L29)\n\n#### Timeline\n\nThe following chart shows that now each render step (re-rendering all components) contains only one update that now re-renders all\ncomponents at once instead of separately.\n\n![Profiler Results - Timeline](./docs/results-2020-05-04/scheduler-render-cycle/profiler-logs-timeline.png)\n\n#### Update \u0026 render durations (all components)\n\nA single update equals a single render. On average, all component re-render at _1.8ms_ to _1.9ms_ time, which breaks down to a theoratical\ncomponent re-render time of _0.01ms_.\n\n![Profiler Results - Update durations](./docs/results-2020-05-04/scheduler-render-cycle/profiler-logs-render-durations.png)\n\n#### Tracing\n\nThis tracing profile looks very clean, very few function calls compared to the not-scheduled test case. The first Macrotask contains the state change and its propagation to the components, the second block runs all scheduled renderings right before the browser renders on screen.\n\n![Tracing Profile](./docs/results-2020-05-04/scheduler-render-cycle/tracing-profile.png)\n\n\u003cbr\u003e\n\n### Bonus: Concurrent Mode (experimental!)\n\nBonus time: Let's test that fancy cool new [React concurrent mode](https://reactjs.org/docs/concurrent-mode-intro.html). Here, we don't have\na manual scheduler in place. Instead, we use the new `ReactDOM.createRoot()` function to instantiate our application, thus enabling\nconcurrent mode, and otherwhise develop our application like we are used to.\n\n\u003e Note: React concurrent mode is still highly experimental, and requires the application to run properly in strict mode!\n\n#### Timeline\n\nThe following chart shows that now each render step (re-rendering all components) contains only one update that now re-renders all\ncomponents at once instead of separately.\n\n![Profiler Results - Timeline](./docs/results-2020-05-04/concurrent-mode/profiler-logs-timeline.png)\n\n#### Update \u0026 render durations (all components)\n\nA single update equals a single render. On average, all component re-render at _1.8ms_ to _1.9ms_ time, which breaks down to a theoratical\ncomponent re-render time of _0.01ms_.\n\n![Profiler Results - Update durations](./docs/results-2020-05-04/concurrent-mode/profiler-logs-update-durations.png)\n\n#### Tracing\n\nThis tracing profile looks very clean, very few function calls compared to the not-scheduled test case. The first Macrotask contains the state change and its propagation to the components. It seems that the React Concurrent Mode decided to schedule re-renderings into a separate Macrotask, using `setTimeout(callbackFn, 0)` - interesting!\n\n![Tracing Profile](./docs/results-2020-05-04/concurrent-mode/tracing-profile.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdominique-mueller%2Freact-render-scheduling-analysis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdominique-mueller%2Freact-render-scheduling-analysis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdominique-mueller%2Freact-render-scheduling-analysis/lists"}