Home >Web Front-end >JS Tutorial >Quick Optimization for your ReactJS Application for Performance and Size
React being massively used for frontend-intensive applications comes with its unique ways of performance and size optimizations. Improving both will have a considerable measurable impact on the React package bundle size. The lower the bundle size, the faster the loading time, considering that we are focusing on client-rendered applications.
Server-side rendering would further improve load time. In server-side rendering when a user requests a web page, the React components are rendered as HTML code in the server itself. Then this pre-rendered page is sent to the browser, allowing the user to see the page immediately without the overhead of the JS loading time.
But that’s a different story altogether. Let’s mainly focus on trying to improve our client-side rendered site by working on improving the Package bundle size by making tweaks in the code. Let’s dive deep.
“Bundling” of React code is the process of following through all imports and codes and combining it into a single file called a ‘Bundle’. Webpack, Browserify, etc., already do this for us.
Webpack has a feature called ‘Code Splitting’ that is responsible for splitting a single bundle into smaller chunks, deduplicating the chunks, and importing them ‘on demand’. This significantly impacts the load time of the application.
module.exports = { // Other webpack configuration options... optimization: { splitChunks: { chunks: 'all', // Options: 'initial', 'async', 'all' minSize: 10000, // Minimum size, in bytes, for a chunk to be generated maxSize: 0, // Maximum size, in bytes, for a chunk to be generated minChunks: 1, // Minimum number of chunks that must share a module before splitting maxAsyncRequests: 30, // Maximum number of parallel requests when on-demand loading maxInitialRequests: 30, // Maximum number of parallel requests at an entry point automaticNameDelimiter: '~', // Delimiter for generated names cacheGroups: { defaultVendors: { test: /[\/]node_modules[\/]/, priority: -10, reuseExistingChunk: true, }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, }, }, };
Lazy Loading Components with React Suspense (React 18): This combined with dynamic imports will show a visible improvement in Component Loading time.
Generally, when we import child components within a parent component, we import it statically. To prevent importing this component till we actually have to render it, we can use a combination of dynamic imports with React Suspense. React Suspense enables loading a component on demand. It shows a Fallback UI while the corresponding components are dynamically imported and then rendered.
import { lazy } from 'react'; // The lazy loaded Component has to be exported as default const BlogSection = lazy(() => import('./BlogSection.tsx')); export default function HomePage() { return ( <> <Suspense fallback={<Loading />}> <BlogSection /> </Suspense> </> ); } function Loading() { return <h2>Component is Loading...</h2>; }
This is a technique used by JavaScript bundlers to remove all unused code before creating bundles. ES6 code can be tree-shaken; however, code that is based out of CommonJS (i.e., uses ‘require’) cannot be tree-shaken.
Webpack Bundle Analyzer is a plugin that will help you visualize the size of a webpack with an interactive map.
npm install --save-dev webpack-bundle-analyzer npm install -g source-map-explorer
Then configure your webpack to add the above as a plugin:
plugins: [ new BundleAnalyzerPlugin(), new HtmlWebpackPlugin({ template: './public/index.html', // Path to your HTML template filename: 'index.html', // Output HTML file name inject: true, // Inject all assets into the body }), ];
Make sure your script is configured to run Webpack:
"build": "webpack --config webpack.config.js --mode production"
Run yarn build to generate a report.html that will help you visualize your bundle size effectively.
It will look something like this:
Let’s start by understanding what Blocking Rendering is. Blocking rendering is when the main thread (UX updates) is blocked because React was doing some less important tasks in the background. This used to be the case till React 16.
React 18 has introduced concurrent features, which means it will:
Use the startTransition() hook to manage React updates as non-urgent, helping React prioritize urgent updates like user-input and user-interaction with components over the prior.
module.exports = { // Other webpack configuration options... optimization: { splitChunks: { chunks: 'all', // Options: 'initial', 'async', 'all' minSize: 10000, // Minimum size, in bytes, for a chunk to be generated maxSize: 0, // Maximum size, in bytes, for a chunk to be generated minChunks: 1, // Minimum number of chunks that must share a module before splitting maxAsyncRequests: 30, // Maximum number of parallel requests when on-demand loading maxInitialRequests: 30, // Maximum number of parallel requests at an entry point automaticNameDelimiter: '~', // Delimiter for generated names cacheGroups: { defaultVendors: { test: /[\/]node_modules[\/]/, priority: -10, reuseExistingChunk: true, }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, }, }, };
In this example, when the input value changes, the handleChange function is called. The startTransition function is used to mark the update to the list state as non-urgent. This allows React to prioritize the update to the value state, ensuring that the input remains responsive even when the list is large.
Use the useDeferredValue hook to defer a value (usually an expensive calculation) until the UI is less busy.
import { lazy } from 'react'; // The lazy loaded Component has to be exported as default const BlogSection = lazy(() => import('./BlogSection.tsx')); export default function HomePage() { return ( <> <Suspense fallback={<Loading />}> <BlogSection /> </Suspense> </> ); } function Loading() { return <h2>Component is Loading...</h2>; }
In this example, the useDeferredValue hook is used to defer the value state until the UI is less busy. This helps keep the input responsive by deferring the rendering of the large list until after the input update is processed.
Key Benefits of Concurrent Rendering:
If you are aware of any heavy resources that your application would be fetching during loading, then a good idea would be to preload the resource. These resources could be fonts, images, stylesheets, etc.
Scenarios where preloading would be beneficial:
module.exports = { // Other webpack configuration options... optimization: { splitChunks: { chunks: 'all', // Options: 'initial', 'async', 'all' minSize: 10000, // Minimum size, in bytes, for a chunk to be generated maxSize: 0, // Maximum size, in bytes, for a chunk to be generated minChunks: 1, // Minimum number of chunks that must share a module before splitting maxAsyncRequests: 30, // Maximum number of parallel requests when on-demand loading maxInitialRequests: 30, // Maximum number of parallel requests at an entry point automaticNameDelimiter: '~', // Delimiter for generated names cacheGroups: { defaultVendors: { test: /[\/]node_modules[\/]/, priority: -10, reuseExistingChunk: true, }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, }, }, };
Interesting fact: After implementing preloading, many sites, including Shopify, Financial Times, and Treebo, saw 1-second improvements in user-centric metrics such as Time to Interactive and User Perceived Latency.
I hope you found this blog helpful! Your feedback is invaluable to me , so please leave your thoughts and suggestions in the comments below.
Feel free to connect with me on LinkedIn for more insights and updates. Let's stay connected and continue to learn and grow together!
The above is the detailed content of Quick Optimization for your ReactJS Application for Performance and Size. For more information, please follow other related articles on the PHP Chinese website!