- Tools for performance testing
- Bundle optimization
- Dynamic reducers
- Code performance improvements
- lighthouse
- bundle-analyzer
Integrated into chrome browsers, tool for running performance checks on websites - gives detailed report about required optimizations. More info: https://developers.google.com/web/tools/lighthouse/
Shows what components are inside each Reacts generated chunk.
Add @scandipwa/bundle-analyzer
into package.json
file:
{
...
"dependencies": {
"@scandipwa/m2-theme": "^0.1.4",
"@scandipwa/bundle-analyzer": "0.0.7",
...
},
"scandipwa": {
...
"extensions": {
"@scandipwa/m2-theme": true,
"@scandipwa/bundle-analyzer": true,
...
},
...
},
...
}
After enabling extension, the bundle analyzer will automatically start together with PWA app. It will run on the next available port (Default: 8002).
(Bundle analyzer will show all chunks, to know what chunks are loaded on each page you can use web browsers developer tools - Debugger or Network)
Bundle optimization - process in witch we reduce chunks size to improve page loading speed, by joining only those components into chunks that are required on request, instead of loading all of them at once.
Splitting chunk into smaller ones can be done in React with two commands:
- lazy - imports component async, imports component when it is called for first time:
...
import { lazy } from 'react';
...
export const MyAccountAddressBook = lazy(() => import(
'Component/MyAccountAddressBook'
));
// Or specify chunk name via 'webpackChunkName'
export const MyAccountAddressBook = lazy(() => import(
/* webpackMode: "lazy", webpackChunkName: "account-address" */
'Component/MyAccountAddressBook'
));
...
- Suspense - used to output fallback component while main component is still loading in:
...
import { Suspense } from 'react';
...
<Suspense fallback={ <Loader /> }>
<MyAccountAddressBook />
</Suspense>
Elements that should be moved to separate chunks for better performance:
-
Pages - Each page should be moved to separate chunk. (As we shouldn't be loading components from different pages into all pages - main chunk)
-
Sections - You can split pages into smaller chunks to prioritize loading of one element before other.
-
Tabs - if page/section contains tab switching (example: MyAccount, ProductDetails ...) then each tab should be moved to separate chunk, thus loading necessary data only on request.
-
Widgets - as widgets can be placed into multiple places, then for best performance each widget should be placed ether in shared widget chunk (widget) or in separate chunk with naming following:
widget-{ name of widget }
, thus loading only necessary components; -
Special case utility - utility that is uesed only in few other chunks can be moved out to seperate chunk.
-
External components - for simpler use external components can be moved to new component that utilizes lazy loading / suspense, thus allowing to use this component from one chunk.
import { Suspense } from 'react';
// Component/ExternalComponent
export const ExternalComponent = lazy(() => import(
/* webpackChunkName: "external-{name}" */
'package'
));
export const renderWithDom(dom) => {
<Suspense fallback={ <Loader /> }>
<ExternalComponent dom>
</Suspense>
};
export const renderWithText(text) => {
<Suspense fallback={ <Loader /> }>
<ExternalComponent text>
</Suspense>
};
- Shared components outside the main chunk - import structure should be analyzed to see what component is used where, thus allowing to move chunks that are shared only between two or three components into one new chunk.
When importing constants into a component the whole file will be added into chunk, thus you should be sure that a specific constant is present in the correct file.
For example when possible you should use .config.js
files to store constants and then import from that instead of splitting them into .container.js
and .config.js
files, as .config.js
file by itself is smaller than .container.js
it will also remove whole file from chunk.
You should investigate "nameless" chunks (chunks that are automatically generated by webpack containing numeric names) and search for chunks with similar import data. Chunk merging can be done in one of the two ways:
- Investigate similar chunks and based on content find the import that is responsible for it in the code base. Then with react lazy loading and webpack chunk naming join them.
- Configure webpack so that smaller packages are joined together.
Sample of webpack configuration to optimize chunks
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 30,
maxSize: 200,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
}
To keep up with principle of only loading what’s needed, we have created new solution for importing reducers into project.
Reducers in project should be splitted into two categories:
- Static - widely used between multiple components
- Dynamic - used for specific cases,
thus instead of loading all reducers in all pages you can split them so that only necessary ones are loaded via new command
withReducers
...
import { withReducers } from 'Util/DynamicReducer';
import CategoryReducer from 'Store/Category/Category.reducer';
...
export default withReducers({
CategoryReducer
})(connect(mapStateToProps, mapDispatchToProps)(CategoryPageContainer));
by using command withReducers the specific reducer is loaded async only when required.
(If reducer is loaded in main component then all exiting / rendered componnent will also have access to this reducer, so best practice should be loading reducers in Page containers)
Reducing components rendering count can improve pages first time load score.
React offers hook shouldComponentUpdate
.
We can look at our current and new props & state and make a choice if we should move on. For example, some components may need to update only after main prop or state has changed and can ignore re-rendering from other changes.
function shouldComponentUpdate(nextProps, nextState) {
const { mainProp } = this.props;
const { mainProp = nextMainProp } = nextProps;
return mainProp !== nextMainProp;
}
On many cases first time load will execute render
method up to eight times, this count can be reduced by performing first time load check together with last updated item check, thus reducing re-rendering count from eight to two.
In many cases all other props and sates are dependent on one specific variable (for example - product), in these cases you can usually perform only specific variable check, although these changes should be tested in-depth, as they can break some functionality.
Since functions are objects in JavaScript ({} !== {})
, the inline function will always fail the prop diff when React does a diff check.
An arrow function will create a new instance of the function on each render if it's used in a JSX property. This might create a lot of work for the garbage collector.
// Bad practice
...
render() {
return (
<Example onClick={(e) => { ... }}>
);
}
...
// Good practice
...
onExampleClick = (example) => {
...
}
render() {
return (
<Example onClick={this.onExampleClick}>
);
}
...
Event trigger rate is the number of times an event handler invokes in a given amount of time. In general, mouse clicks have lower event trigger rates compare to scrolling and mouseover. Higher event trigger rates can sometimes crash your application, but it can be controlled.
In a nutshell, throttling means delaying function execution. So instead of executing the event handler/function immediately, you’ll be adding a few milliseconds of delay when an event is triggered.
Unlike throttling, debouncing is a technique to prevent the event trigger from being fired too often.
When performing async calls in componentWillMount
hook the function will lack access to refs and DOM element, instead of using this hook use alternative that will be fired after render - componentDidMount
.
Avoid setting state values from props in constructor otherwise, you will lose linkage.
By using React.memo
, we can store component into memory and on recall will perform a shallow equal comparison of both props and context of the component based on strict equality, and based on that will ether load existing component or repopulate and re-render it.
Allows you to memoize expensive functions so that you can avoid calling them on every render. You simple pass in a function and an array of inputs and useMemo will only recompute the memoized value when one of the inputs has changed.
import { useMemo } from "react"
...
const test = useMemo(() => expensiveComputation(parameter), [parameters]);
There are 3 ways of perfoming animations in web browser (sorted by performance cost):
- CSS transitions
- CSS animations
- JavaScript Most modern browsers are already optimized for handling CSS animation, however js animations aren't, thus requiring creator to optimize them themself.
Web Workers makes it possible to run a script operation in a web application’s background thread, separate from the main execution thread. By performing the laborious processing in a separate thread, the main thread, which is usually the UI, can run without being blocked or slowed down.
In the same execution context, as JavaScript is single threaded, we will need to parallel compute. This can be achieved two ways. The first option is using pseudo-parallelism, which is based on setTimeout function. The second option is to use Web Workers.
(Although most computation / data preparation should be done on the server side (as they can be cached) there may be some exceptions in which moving some tasks to separate "thread" can improve main thread load.)
// Bad practice
function sort (products) {
for (...)
for (...) {
const tmp = product[x];
product[x] = product[y];
product[y] = tmp;
}
}
...
sortProducts = () => {
const { products } = this.state;
this.setState({
products: sort(products)
});
}
render() {
const { products } = this.state;
return (
<>
<Button onClick={this.sortProducts} />
<Products products={products}>
</>
)
}
// Good practice
function sort (products) {
self.addEvenetListener('message', e=> {
for (...)
for (...) {
const tmp = product[x];
product[x] = product[y];
product[y] = tmp;
}
postMessage(products);
});
}
...
componentDidMount() {
this.worker = new Worker('sort.worker.js');
this.worker.addEventListener('message', event => {
const sortedProducts = event.data;
this.setState({
products: sortedProducts
})
});
}
sortProducts = () => {
const { products } = this.state;
this.worker.postMessage(products);
}
render() {
const { products } = this.state;
return (
<>
<Button onClick={this.sortProducts} />
<Products products={products}>
</>
)
}
When rendering large lists of data, it is recomended that only visible portion of elements is outputed and all other elements are loaded when viewport enters view.
// Bad practice
render() {
if (test === 'test') {
return (
<>
<A />
<B />
<C />
</>
);
} else {
return (
<>
<B />
<C />
</>
);
}
}
// Good practice
render() {
return (
<>
{ test === "test" && <A /> }
<B />
<C />
</>
);
}
In the "Bad practice" example conditional operator and if else
condition seems to be fine but it has a performance flaw.
Each time the render function is called and the value toggles between "test" and another value, a different if else statement is executed.
The diffing algorithm will run a check comparing the element type at each position. During the diffing algorithm, it seems that the A is not available and the first component that needs to be rendered is B.
React will observe the positions of the elements. It seems that the components at position 1 and position 2 have changed and will unmount the components.
The components B and C will be unmounted and remounted on position 1 and position 2. This is ideally not required, as these components are not changing, but still, we have to unmount and remount these components, wich is a costly operation.
- JS
- CSS
- Fonts