React technical principles and code development practice: Configuring and using Webpack in React projects

Author: Zen and the Art of Computer Programming

1. Background Introduction

React is one of the most popular front-end frameworks at the moment, and some basic knowledge about it has been released on its official website. React developers are increasingly joining the ranks. There are a large number of articles involving React in many places, such as learning resources, open source projects, workplace skills, etc. But for ordinary technicians, it is not easy to correctly understand React as a technology stack and get started with it quickly. And many junior technicians even find React difficult to understand. Therefore, this article attempts to help technicians better understand the workflow of React and how to configure and use Webpack to optimize the performance of React applications by sharing experiences and tutorials.

2. Core concepts and connections

React introduction

React is a JavaScript library for building user interfaces. It is mainly used to build dynamic interfaces. React uses JSX (JavaScript Extension) syntax to provide declarative programming and enable web pages to have a componentized structure. React can render HTML, as well as SVG and Canvas. React provides many excellent features, such as virtual DOM, componentization, one-way data flow, etc.

Introduction to Webpack

Webpack is a module bundler. It can combine multiple modules into a file or an application that the browser can run directly. Webpack can handle all types of resources, including JavaScript modules, style sheets, images, fonts, etc. Webpack can convert these resources into a format that the browser can recognize, such as JS files, CSS files, or images in the browser. Webpack can use loaders to preprocess files at compile time, and you can also use plugins to extend its functionality.

The relationship between React and webpack

Webpack is an integral part of the React ecosystem. Webpack can package different modules used in React projects into different files and then provide them to the browser. Since Webpack is an independent tool that can help React developers package a variety of project files, it is very suitable for large and complex projects. The relationship between Webpack and React is like the relationship between the human intestine and bone marrow. Without Webpack, React projects will not function properly; without React, Webpack will also be meaningless.

3. Detailed explanation of core algorithm principles, specific operation steps and mathematical model formulas

This article will introduce the performance optimization of Webpack and React applications from the following aspects:

  • Configure Webpack
  • Using Babel
  • Tree Shaking
  • Lazy loading/on-demand loading
  • Server side rendering SSR
  • Use of online debuggable tools First, we need to install the Nodejs environment. Next, we can install Webpack and the React scaffolding create-react-app through the command. As shown below:
    npm install -g webpack react-scripts

    Create a React project and enter the project root directory, execute the following command to initialize the project:

    npx create-react-app my-app
    cd my-app

    Through Webpack, we can realize automated construction of React projects, compress and merge static resource files, and package optimized code. Webpack’s configuration file is webpack.config.js. An example of the configuration file is as follows:

    const path = require('path');
    module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.[hash].js',
      path: path.resolve(__dirname, 'build')
    },
    module: {
      rules: [
        {
          test: /\.jsx?$/, // Match files ending with .js or .jsx
          exclude: /node_modules/, // Do not package files in node_modules
          use: ['babel-loader'] // Use babel-loader to compile jsx files
        }
      ]
    }
    };

    The entry attribute here specifies the entry file path, and output specifies the name and location of the output file. The module.rules array defines file matching rules. The test attribute matches whether the file name meets the requirements, and the exclude attribute filters folders (node_modules) that do not need to be packaged. The use array element specifies which loader to use to compile the matching file. babel-loader is a loader used to compile JSX.

Below, we use the Tree Shaking mechanism to reduce useless code. Tree Shaking can analyze your import and export statements and remove unused code blocks, that is, only keep the code that is used. So, just import the React functions you actually use. The modified webpack.config.js file is as follows:

const path = require('path');
module.exports = {
  mode: "production", // The default mode is development. In production mode, optimization options such as compression and obfuscation are enabled.
  optimization:{
    usedExports: true // enable tree shaking
  },
  entry: './src/index.js',
  output: {
    filename: 'bundle.[hash].js',
    path: path.resolve(__dirname, 'build'),
    publicPath: '/' // Allow CDN to access the path when importing resources
  },
  module: {
    rules: [{
      test: /\.jsx?$/, // Match files ending with .js or .jsx
      exclude: /node_modules/, // Do not package files in node_modules
      use: ['babel-loader'] // Use babel-loader to compile jsx files
    }]
  },
  devtool:'source-map' // Generate Source Map for easy debugging
}

In addition to the above optimization methods, we can also use Babel to translate ES6 code into ES5 code, which can make browsers of IE8 and earlier versions run better.

For lazy loading and on-demand loading, we can use Webpack’s asynchronous import to achieve this, which can effectively reduce the initial page loading time. As follows, we first import the component into the main entry file, and then import other components asynchronously:

import React from'react';
import ReactDOM from'react-dom';
// main component import here...
import MyComponent from './MyComponent';

class App extends React.Component{
  render(){
    return <div>
      {/* other components import async*/}
      <Suspense fallback={<div>Loading...</div>}><LazyComponent/></Suspense>
      <hr/>
      <MyComponent />
    </div>;
  }
}

Finally, in order to improve application performance, you can use Server Side Rendering SSR. SSR can send the data required for initial routing directly to the client upon request, avoiding the need to wait for the entire React application to be downloaded and parsed before displaying the page. We need to modify the server configuration to support SSR. The modified express server example is as follows:

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('*', (req, res) => {
  const context = {};

  const html = ReactDOMServer.renderToString(<App />);

  res.send(`
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>React SSR</title>
    </head>
    <body>
      <div id="root">${html}</div>
      <!-- the following script tag is for server side rendering -->
      <script src="/static/main.${process.env.BUNDLE_HASH}.js"></script>
    </body>
    </html>`);
});

if (process.env.NODE_ENV === 'production') {
  app.use(express.static(path.join(__dirname, '..', 'build')));
} else {
  app.use(express.static(path.join(__dirname, '..', 'public')));
}

app.listen(port, () => console.log(`Listening on ${port}`));

Here we put the compiled files of the React application in the public folder. It should be noted that the service listening port will only be started in the production environment.

Finally, for online debugging tools, we can install Redux DevTools to view the Redux data flow, and use React Developer Tools to view the rendering of components. In the Chrome browser, after installing the extension, you can see the status changes of Redux at the bottom of the Elements tab bar. Click on a component to check the component’s props and state values on the right. as follows:

4. Specific code examples and detailed explanations

In this section, we will take creating a simple counter as an example and practice it through performance optimization methods of Webpack and React applications.

Installation dependencies

First, we need to install the dependencies:

npm i -S prop-types --save-dev # prop-types is used to facilitate PropTypes to check the type. 

Create two files, Counter.jsx and index.js. Counter.jsx is the counter component, and index.js is the React application entry file.

Counter component writing

import React, { useState } from'react';
function Counter() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount((prevState) => prevState + 1);
  }
  return (
    <>
      <h1>{count}</h1>
      <button onClick={handleClick}> + </button>
    </>
  );
}
export default Counter;

The Counter component has two state variables: count and setCount, which represent the current count value and the set new value function respectively. The handleClick function responds to the button click event and calls setCount to increase the count value.

index.js written

import React from'react';
import ReactDOM from'react-dom';
import { BrowserRouter as Router } from'react-router-dom';
import Counter from './components/Counter';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <Router>
    <React.StrictMode>
      <Counter />
    </React.StrictMode>
  </Router>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

In the index.js file, we first import the dependencies and then import the Counter component. We also use BrowserRouter to implement the client’s routing function. Finally, we use the ReactDOM.render method to render the component to the specified DOM node root. We also added a line at the end of the reportWebVitals method, which captures rendering, updates, animation frame rates, etc. and sends them to our Analytics platform.

Startup project

npm run start

We can visit http://localhost:3000 in the browser to see the effect.

Modify configuration file

--- a/webpack.config.js
 + + + b/webpack.config.js
@@ -4,6 + 4,10 @@ const path = require('path');

 module.exports = {
   entry: ['./src/index.js'],
 + mode:"production", // The default mode is development, and optimization options such as compression and obfuscation are enabled in production mode.
 + optimization:{
 + usedExports: true // enable tree shaking
 + },
   output: {
     filename: '[name].[contenthash].js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
@@ -17,6 + 21,13 @@ module.exports = {
         },
       ],
     }),
 + ],
 + resolve: {
 + extensions: ['.js', '.jsx'], // The order in which webpack searches for modules, matching these suffix names from left to right.
 + },
 + stats: "errors-only" // Only error messages are displayed
 };

We changed the default development mode to production mode and turned on Tree Shaking. In order to implement Webpack’s loading method for asynchronous modules, we specified the module suffix names ‘.js’ and ‘.jsx’. Additionally, we specify stats as “errors-only” to display only error messages.

Optimize performance

We can use Webpack’s code splitting technology to split the code. Through code splitting, we can split the code into multiple files and then load them on demand, which can effectively reduce the first screen rendering time. We can use the HtmlWebpackPlugin plug-in to automatically inject asynchronously loaded script files when generating the final product.

--- a/package.json
 + + + b/package.json
@@ -25,7 + 25,8 @@
     "start": "react-scripts start",
     "build": "react-scripts build",
     "test": "react-scripts test",
- "eject": "react-scripts eject"
 + "eject": "react-scripts eject",
 + "split-chunks-plugin": "^1.4.9"
   },
   "devDependencies": {
     "@testing-library/jest-dom": "^5.11.9",
@@ -33,6 + 34,7 @@
     "prettier": "^2.2.1",
     "prop-types": "^15.7.2",
     "react-dom": "^17.0.1",
 + "split-chunks-plugin": "^1.4.9"
   }
 }

We install the split-chunks-plugin plugin to implement code splitting. Modify the webpack.config.js file as follows:

--- a/webpack.config.js
 + + + b/webpack.config.js
@@ -4,6 + 4,12 @@ const path = require('path');

 module.exports = {
   entry: ['./src/index.js'],
 + mode:"production", // The default mode is development, and optimization options such as compression and obfuscation are enabled in production mode.
 + optimization:{
 + usedExports: true // enable tree shaking
 + },
   output: {
     filename: '[name].[contenthash].js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
@@ -11,11 + 17,15 @@ module.exports = {

   module: {
     rules: [
       {
- test: /\.jsx?$/,
 + test: /\.[j|t]sx?$/,
         exclude: /node_modules/,
 + include: path.resolve(__dirname,'src'), // Specify the source code directory
         use: {
           loader: 'babel-loader',
           options: {
 + cacheDirectory: true,
             presets: ["@babel/preset-env", "@babel/preset-react"],
 + }
 + }
       },
     ],
   },
@@ -32,6 + 42,8 @@ module.exports = {

       new HtmlWebpackPlugin({
         title: 'React SSR',
 + chunks:['main'], // The name of the chunk that needs to be loaded asynchronously
 + minify: { removeAttributeQuotes: false, collapseWhitespace: false}, // Remove spaces and quotes
         template: "./public/index.html",
         favicon: "./public/favicon.ico",
       }),

In production mode, we specify the chunk to load after code splitting and add the webpack configuration to package.json. We also configured Babel to compile JSX to JavaScript. The modified webpack.config.js file is as follows:

--- a/webpack.config.js
 + + + b/webpack.config.js
@@ -4,6 + 4,12 @@ const path = require('path');

 module.exports = {
   entry: ['./src/index.js'],
 + mode:"production", // The default mode is development, and optimization options such as compression and obfuscation are enabled in production mode.
 + optimization:{
 + usedExports: true // enable tree shaking
 + },
   output: {
     filename: '[name].[contenthash].js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
@@ -11,11 + 17,15 @@ module.exports = {

     // loaders and plugins are configurations of Webpack

     module: {
 + name: 'client', // Configuration name
 + type: 'async', // Set the Chunk type to an asynchronous loading type
 + chunks: 'all', // All chunks from all entry points to the target chunk
         rules: [
           {
             test: /\.[j|t]sx?$/,
 + include: path.resolve(__dirname,'src'), // Specify the source code directory
             exclude: /node_modules/,
             use: {
               loader: 'babel-loader',
               options: {
                 cacheDirectory: true,
                 presets: ["@babel/preset-env", "@babel/preset-react"],
@@ -32,6 + 42,8 @@ module.exports = {

         new HtmlWebpackPlugin({
           title: 'React SSR',
 + inject:true,//Default false If true, a link tag will be injected before the output file, and the link will point to the final output bundle file. If set to true, the chunks configuration must be a string or string array. This configuration is not recommended. It is recommended to use the chunks parameter of HtmlWebpackPlugin.
           chunks:['main'], // The name of the chunk that needs to be loaded asynchronously
           minify: { removeAttributeQuotes: false, collapseWhitespace: false}, // Remove spaces and quotes
           template: "./public/index.html",

Finally, we restart the project and observe the output of Webpack. Through the console log of Webpack, we can clearly see that Webpack splits multiple bundle files according to the logic of code splitting, and each file is optimized accordingly. As shown below: