Below are notes taken while researching 'React Performance' over the past quarter. All resources used are posted at the bottom of the page. This includes many articles, videos, and tutorials.
- Code splitting
- Avoiding unnecessary renders
- Slow renders
- Long lists
- Fixing death by a thousand cuts
- Optimizing Redux
- Optimizing Context
- Tools for Measuring Performance
- Definitions
- Resources
- Performance optimizations have a cost and are intended for special cases only
-
Code Splitting is a feature which can create multiple bundles that can be dynamically loaded at runtime.
-
Code-splitting your app can help you “lazy-load” just the things that are currently needed by the user, and reducing the amount of code needed during the initial load.
import('/some-module.js').then(
(module) => {
// do stuff with the module's exports
},
(error) => {
// there was some error loading the module...
}
)
- React has built-in support for loading modules as React components.
const LineChart = React.lazy(() => import('./line-chart'))
function App() {
return (
<div>
<React.Suspense fallback={<div>loading...</div>}>
<LineChart />
</React.Suspense>
</div>
)
-
Tools to help determine need/benefit of code splitting
-
Dev tools "Coverage" feature
-
Webpack Bundle Analyzer
-
Webpack Prefetching/Preloading Modules
- prefetch: resource is probably needed for some navigation in the future
- preload: resource will also be needed during the current navigation
- Magic Comments
-
import(/* webpackPrefetch: true */ './path/to/LoginModal.js')
<link rel="prefetch" href="login-modal-chunk.js">
-
Very often, problems are related to unnecessary rendering
-
React does not control renders by default
-
Areas where re-renders could matter
- Rendering charts
- Parent components that control many children
-
How to determine unnecessary renders
- React Profiler
- Highlight updates
-
How to control renders
-
Pure Component
- when prop are not changing fequently
- when component isn't very complex
Example
export class ClassComponent extends React.PureComponent { // code here } export const FunctionalComponent = React.memo((props) => { // code here })
-
Must watch how props are being passed (follow best practices)
- example that breaks reference equality
export const Panel = ({ width, onPanelClose, ...props }) => ( <CustomChart title="something" chartStyle={{ color: 'red', width }} onClose={() => { console.log('closing') onPanelClose() }} /> )
A classical solution?
export class Panel extends React.PureComponent { constructor(props) { super(props) this.state = { chartStyle: { color: 'red', width: props.width } } } componentDidUpdate(prevProps) { const { width } = this.props if (width !== prevProps.width) this.setState({ chartWidth: { color: 'red', width } }) } closePanel = () => { console.log('close this') this.props.onPanelClose() } render() { return ( <CustomChart title="something" chartStyle={this.state.chartStyle} onClose={this.closePanel} /> ) } }
Using hooks
export const Panel = React.memo(({ width, onPanelClose, ...props }) => { const chartStyle = useMemo(() => ({ color: 'red', width }), [width]) const closePanel = useCallback(() => { console.log('close this') onPanelClose() }, [onPanelClose]) return ( <CustomChart title="something" chartStyle={childStyle} onClose={closePanel} /> ) })
Redux Example (breaking reference equality)
const unfriend = (name) => ({ type: 'UNFRIEND', name }) const mapStateToProps = (state, ownProps) => { const { friendNames } = ownProps const friends = friendNames.map((name) => state.user[name]) const numFriendRequests = state.friendRequests.length return { friends, numFriendRequests } } const mapDispatchToProps = (dispatch) => ({ unfriend: (name) => dispatch(unfriend(name)) }) export const FriendsComponent = connect( mapStateToProps, mapDispatchToProps )(FriendList)
Redux Example using memoization
const mapStateToProps = (state, ownProps) => { const { friendNames } = ownProps const friends = memoize( () => friendNames.map((name) => state.user[name]), [friendNames, state.user] ) const numFriendRequests = state.friendRequests.length return { friends, numFriendRequests } } const mapDispatchToProps = { unfriend }
-
-
Prop Filtering
- restrict props passed to the component so it doesn't update because of a prop it doesn't use
example
const PropFilteredFriendList = ({ friends, friendNames, ...props }) => ( <FriendList friends={friends} /> )
Fix the slow render before you fix the re-render
-
useMemo
-
Calculations performed within render will be performed every render, regardless of change. The same goes for functional component.
-
Hook that memorizes the output of a function
-
This is different from useEffect in that useEffect is intended for side effects, while functions in useMemo are supposed to be pure w/o side effects
const CustomChart = (x, y) => { const data = processData(x, y) return <Chart data={data} /> }
const CustomChart = (x, y) => { const data = React.useMemo(() => processData(x, y), [x, y]) return <Chart data={data} /> }
-
-
- Javascript is single-threaded. This means that any javascript environment will not run multiple lines of javascript in the same process simultaneously.
- App runs on the main thread, web workers runs on separate thread
- Issues: serialization costs with communication
-
WASM (web assembly)
- open standard that defines a portable binary-code format for executable programs, and a corresponding textual assembly language, as well as interfaces for facilitating interactions between such programs and their host environment
- Compilation target for languages such as C,C++, rust
React does a great job at batching DOM updates and only updating what needs to be changed. However, if you need to make HUGE updates to the DOM there isn't much React can do to help.
List "virtualization" focuses on just rendering items visible to the user. This works by maintaining a window and moving that window around your list.
- Tradeoffs
- Not searchable with ctrl-F
- accessibility challenges
- heavy lists can sometimes show flashing or artifacts if rendering can't keep up
Death by a thousand cuts means so many components are updated when state changes that it becomes a performance bottleneck.
Fixing "perf death by a thousand cuts" by colocating state
Example
export const Application = () => {
const [dog, setDog] = React.useState('')
const [time, setTime] = React.useState(200)
return (
<div>
<DogName time={time} dog={dog} onChange={setDog} />
<SlowComponent time={time} onChange={setTime} />
</div>
)
}
State Colocation will make your React app faster
Moving the state as close to the component using it works in the example above.
However, there may be times where we need to lift
the state to make it
accessible by multiple components. What can be done to improve performance in
this scenario?
Separating the state:
- swap
useState
withuseReducer
- create action type and action creator
- wrap action creators in
useCallback
- Wrap certain components in
React.memo
- See example here.. GitHub - stevekinney/grudges-react-state at exercise-grant-forgiveness
- Deep cloning state Performance | Redux
- Normalize state (Object vs Array?) Redux Essentials, Part 6: Performance and Normalizing Data | Redux
- Potentially use
React.useMemo
to memoize the context value - Separate the contexts (
state
in one context provider, anddispatch
function in another context provider)
-
- To help observe performance problems, click settings gear and change CPU throttle.
-
- API that lests you define precise performance marks
- Marks can be named and are displayed in DEV Tools
- Often easier to read than trying to drill down into the task flame graph
performance.mark('start') // expensive calculation here performance.mark('stop') performance.measure('mainthread', 'start', 'stop')
-
One great way to analyze your app to determine the need/benefit of code splitting for a certain feature/page/interaction, is to use the “Coverage” feature of the developer tools Find Unused JavaScript And CSS Code With The Coverage Tab In Chrome DevTools
- React lifecycle
- The “render” phase: create React elements React.createElement
- The “reconciliation” phase: compare previous elements with the new ones
- The “commit” phase: update the DOM (if needed).
- Pure Component
- renders the same output for the same state and props
- Memoization
- optimization technique that stores the results of expensive function calls and returns the cached reuslt when the same inputs occur again
- Shallow comparison
- a shallow comparison will check that primitives have the same value (eg, 1 equals 1 or that true equals true) and that the references are the same between more complex javascript values like objects and arrays.
- GitHub - kentcdodds/react-performance
- React Optimization Tips and Tricks - Time To React - May 2019 - YouTube
- Performance Is Magic - How To Make Your React App Performant || Ken Wheeler - YouTube
- When to useMemo and useCallback
- Optimizing Performance – React
- Profiling React component performance with Chrome devtools - Calibre
- https://github.com/stevekinney/grudges-react-state
- Profiling React.js Performance