记一次 React 项目的优化(webpack4 插件的使用)

简介:

这里记录了自己在开发一个 React 项目时使用 Webpack 优化项目的过程,欢迎大家围观点赞或吐槽。

学习 React 时候,写了个个人博客站点。使用 webpack 作为打包工具,在这之前学习 webpack 时候,知道 webpack 有插件可以做资源压缩、抽离,以达到减小资源的体积,便于缓存资源的目的,但是开始这个项目时候并没有想立即使用 webpack 的插件带来的便利,主要是想先写完再来优化,也便于优化前后有个对比,便于深入的了解插件的作用。

源码地址: GiteeGithub

写完后项目打包后的main.js文件体积是 3.38 MiB,我部署使用的腾讯云 1 M 带宽的服务器,访问速度很慢。

看看此时 webpack.config.js 的配置:


const path = require('path');
var webpack = require('webpack');
const config = {
  entry: ['babel-polyfill','./src/app.js'],
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js',     // 打包输出,单文件输出
    publicPath: '/',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        include: /(src)/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader'],
      },
      {
        test: /\.scss$/,
        use: [{
          loader: 'style-loader',
        }, {
          loader: 'css-loader',
        }, {
          loader: 'sass-loader',
        },],
      },
    ],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ],
  devServer: {
    contentBase: path.join(__dirname, "build"),
    compress: true,
    port: 9000,
    open: true,
    inline: true,
  },
};

module.exports = config;

npm run build  结果:


 Asset      Size  Chunks             Chunk Names
main.js  3.38 MiB    main  [emitted]  main

3 M 的单文件实在是太大了。适当减少请求次数,减少单次请求的文件大小,这是做前端优化的重要手段。如何缩小这个单页面应用体积,或者适当拆分资源(利用浏览器可以同时下载多个资源的特性)来优化访问速度。

js压缩

去掉注释,减少空格可以减少无用字符占用的文件体积。webpack 插件 UglifyjsWebpackPlugin 官方对插件的介绍是用来缩小你 javascript 文件,对于我这个博客而言就是 main.js 文件。

webpack.config.js 配置如下:



const path = require('path');
var webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const config = {
  entry: ['babel-polyfill','./src/app.js'],
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js',
    publicPath: '/',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        include: /(src)/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader'],
      },
      {
        test: /\.scss$/,
        use: [{
          loader: 'style-loader',
        }, {
          loader: 'css-loader',
        }, {
          loader: 'sass-loader',
        },],
      },
    ],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new UglifyJsPlugin({
      uglifyOptions: {
        ie8: false,
        mangle: true,
        output: { comments: false },
        compress: {
          warnings: false,
          drop_console: true,
          drop_debugger: true,
          unused: false,
         },
      },
      sourceMap: true,
      cache: true,
    }),
  ],
  devServer: {
    contentBase: path.join(__dirname, "build"),
    compress: true,
    port: 9000,
    open: true,
    inline: true,
  },
};

module.exports = config;

npm run build  输出:

  Asset     Size    Chunks             Chunk Names
main.js  3.1 MiB    main  [emitted]  main

可见资源减少了 0.28 MIB。

gzip 压缩

上面的 UglifyjsWebpackPlugin 插件带来的压缩效果可能并不能满足我们的要求。我们熟悉有一种打包压缩方式,将文件压缩为 zip 包,这种压缩效果显著,通常可以将文件成倍压缩,那么这种压缩方式能否在这里使用呢,答案是可以的。CompressionWebpackPlugin 插件就提供了这种功能,我们来引入看看效果。

webpack.config.js 配置如下:


const path = require('path');
var webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CompressionPlugin = require("compression-webpack-plugin");

const config = {
  entry: ['babel-polyfill','./src/app.js'],
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js',
    publicPath: '/',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        include: /(src)/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader'],
      },
      {
        test: /\.scss$/,
        use: [{
          loader: 'style-loader',
        }, {
          loader: 'css-loader',
        }, {
          loader: 'sass-loader',
        },],
      },
    ],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new UglifyJsPlugin({
      uglifyOptions: {
        ie8: false,
        mangle: true,
        output: { comments: false },
        compress: {
          warnings: false,
          drop_console: true,
          drop_debugger: true,
          unused: false,
         },
      },
      sourceMap: true,
      cache: true,
    }),
    new CompressionPlugin(),
  ],
  devServer: {
    contentBase: path.join(__dirname, "build"),
    compress: true,
    port: 9000,
    open: true,
    inline: true,
  },
};

module.exports = config;

npm run build  结果:
....
 Asset     Size  Chunks             Chunk Names
   main.js  3.1 MiB    main  [emitted]  main
main.js.gz  544 KiB          [emitted]
....

可以看到,多出一个 main.js.gz 压缩包,只有 554 KiB,很惊喜有没有?只不过要使用这种压缩文件,nginx 需要配置支持,nginx 的 nginx_http_gzip_static_module 模块可以支持请求压缩包,nginx 配置如下:


...
server {
    gzip on;
    gzip_static on;
    gzip_min_length 1000;
    gzip_buffers 4 8k;
    gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss image/jpeg image/png image/g
    gzip_vary on;
    listen 80;
    location / {
    	...
    }
  }
...

这样浏览器端下载的资源就由原来的 3.38M 降到了 554K。

main.js 是个大杂烩 — css 提取

已经将文件压缩成 gzip 文件,从减少文件体积方面好像已经无计可施。但回头看看 main.js 文件,不难发现他是个即有 js 又有 css 的大杂烩,要是能把 css 抽离出来,是不是可以进一步减少单文件体积,虽然会多出一个 css 文件,多了次请求,但正好利用了浏览器的并发下载,从缓存资源角度来讲也是有利的。webpack 插件 extract-text-webpack-plugin 可以用来提取 css。但是需要注意的是 extract-text-webpack-plugin 只能用在 webpack 4 以下,webpack4 及以上版本需要使用 mini-css-extract-plugin

webpack.config.js 配置如下:


const path = require('path');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CompressionPlugin = require("compression-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const config = {
  entry: ['babel-polyfill','./src/app.js'],
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name].[hash:8].js',
    publicPath: '/',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        include: /(src)/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html',
    }),
    new webpack.HotModuleReplacementPlugin(),
    new UglifyJsPlugin({
      uglifyOptions: {
        ie8: false,
        mangle: true,
        output: { comments: false, },
        compress: {
          warnings: false,
          drop_console: true,
          drop_debugger: true,
          unused: false,
         },
      },
      sourceMap: true,
      cache: true,
    }),
    new CompressionPlugin(),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:8].css",
      chunkFilename: "[id].[hash:8].css",
    }),
  ],
  devServer: {
    contentBase: path.join(__dirname, "build"),
    compress: true,
    port: 9000,
    open: true,
    inline: true,
  },
};

module.exports = config;

npm run build: 

Built at: 2018-06-21 08:03:57
               Asset       Size  Chunks             Chunk Names
   main.b2c90941.css    317 KiB    main  [emitted]  main
    main.b2c90941.js   2.69 MiB    main  [emitted]  main
          index.html  263 bytes          [emitted]
       index.html.gz  196 bytes          [emitted]
main.b2c90941.css.gz   33.4 KiB          [emitted]
 main.b2c90941.js.gz    501 KiB          [emitted]

可见 css 被取了出来,这里的提取效果没有想想中的理想,js 的体积缩小的并不多,原本 css 也不多。

html 文件自动生成: 抽离后文件自动在 html 页面引入

上一步抽离了 css,在打包的使用启用了 chunk hash,这样当 js 或者 css 文件有改动后运行 npm run build 每次生成的 js 和 css 的打包文件名是不同的,这样就有两个问题需要解决: 1、每次 build 后需要在 index.html 页面修改 css 和 js 文件的名称,2、多次修改后 build,会产生需要没用的 js、css 文件。针对这两个问题,大牛早已经给出了解决方案。

html-webpack-plugin 插件可以让我们指定生成 index.html 使用的模版文件,build 后会自动生成 index.html 文件,并将 css 和 js 文件自动引入到 html 文件。

clean-webpack-plugin 插件则可以帮我们在 build 的开始阶段,自动删除指定的目录或者文件。

webpack.config.js 配置如下:


const path = require('path');
var webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CompressionPlugin = require("compression-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const config = {
  entry: ['babel-polyfill','./src/app.js'],
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name].[hash:8].js',
    publicPath: '/',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        include: /(src)/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
        ],
      },
    ],
  },
  plugins: [
    // build 开始阶段需要删除的文件
    new CleanWebpackPlugin(['build/*'], { 
      watch: true,
    }),
    // 指定生成 html 文件使用的模版文件
    new HtmlWebpackPlugin({
      template: 'index.html',
    }),
    new webpack.HotModuleReplacementPlugin(),
    new UglifyJsPlugin({
      uglifyOptions: {
        ie8: false,
        mangle: true,
        output: { comments: false },
        compress: {
          warnings: false,
          drop_console: true,
          drop_debugger: true,
          unused: false,
         },
      },
      sourceMap: true,
      cache: true,
    }),
    new CompressionPlugin(),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:8].css",
      chunkFilename: "[id].[hash:8].css",
    }),
  ],
  devServer: {
    contentBase: path.join(__dirname, "build"),
    compress: true,
    port: 9000,
    open: true,
    inline: true,
  },
};

module.exports = config;

npm run build

...
// 删除指定的文件
clean-webpack-plugin: /Users/wewin/reactLearn/redux-blog/build/* has been removed.
...
               Asset       Size  Chunks             Chunk Names
   main.ad06a35d.css    317 KiB    main  [emitted]  main
    main.ad06a35d.js   2.69 MiB    main  [emitted]  main
          index.html  265 bytes          [emitted]
       index.html.gz  196 bytes          [emitted]
main.ad06a35d.css.gz   33.4 KiB          [emitted]
 main.ad06a35d.js.gz    501 KiB          [emitted]
...

自动生成的 index.html 文件:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>redux blog</title>
    <link href="/main.ad06a35d.css" rel="stylesheet">
  </head>
  <body>
    <div id="root"></div>
    <script type="text/javascript" src="/main.ad06a35d.js"></script>
  </body>
</html>

js 和 css 文件被自动引入到了 html 页面,上次 build 生成的文件,会自动被删除。

被打包的文件里到底有些什么 --- BundleAnalyzerPlugin

上面对文件的压缩,css 的提取都起到了减少 js 提交的作用,但是经过上面两个步骤后,最后打包的 main.js 仍有 501 KiB,想要进一步减少文件体积,我们就要清楚 main.js 文件里到底有些什么,是什么导致了文件如此庞大。BundleAnalyzerPlugin 插件可以帮我们分析出文件的组成,可以以文件的或者网页的形式展示给我们。配置和使用这里不做具体的说明。

公共 JavaScript 模块抽离

将公共的 JavaScript 模块抽离,避免重复的引入,可以有效的减少 js 文件体积。webpack 4 可以使用 SplitChunksPlugin 插件来提取共同的 js,在 webpack 4 以下版本可以使用 CommonsChunkPlugin 插件。

webpackge.config.js



const path = require('path');
var webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CompressionPlugin = require("compression-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const config = {
  entry: ['babel-polyfill', './src/app.js'],
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name].[hash:8].js',
    publicPath: '/',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
        ],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(['dist', 'build/*'], {
      watch: true,
    }),
    new HtmlWebpackPlugin({
      template: 'index.html',
    }),
    new webpack.HotModuleReplacementPlugin(),
    new UglifyJsPlugin({
      uglifyOptions: {
        ie8: false,
        mangle: true,
        output: { comments: false },
        compress: {
          warnings: false,
          drop_console: true,
          drop_debugger: true,
          unused: false,
         }
      },
      sourceMap: true,
      cache: true,
    }),
    new CompressionPlugin(),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:8].css",
      chunkFilename: "[id].[hash:8].css",
    }),
  ],
  optimization: {
    splitChunks: {
      chunks: 'initial',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 2,
      maxInitialRequests: 2,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /\/node_modules\//,
          priority: -10,
        },
        'react-vendor': {
          test: (module, chunks) => /react/.test(module.context),
          priority: 1,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        }
      }
    }
  },
  devServer: {
    contentBase: path.join(__dirname, "build"),
    compress: true,
    port: 9000,
    open: true,
    inline: true,
  },
};

module.exports = config;

npm run build


                            Asset       Size             Chunks             Chunk Names
                 main.3413403b.js   8.33 KiB               main  [emitted]  main
   react-vendor~main.3413403b.css    318 KiB  react-vendor~main  [emitted]  react-vendor~main
    react-vendor~main.3413403b.js   2.68 MiB  react-vendor~main  [emitted]  react-vendor~main
                       index.html  355 bytes                     [emitted]
              main.3413403b.js.gz    3.2 KiB                     [emitted]
                    index.html.gz  214 bytes                     [emitted]
react-vendor~main.3413403b.css.gz   33.5 KiB                     [emitted]
 react-vendor~main.3413403b.js.gz    498 KiB                     [emitted]

这里对 splitChunks 的配置基本上使用的基本都是默认配置,splitChunks 的使用可以参考官网。提取 js 不但可以缩小文件体积,对 React React-dom 这种基础依赖的提取更有利于缓存。

这里主要是记录下自己在减少打包后的文件体积使用到的 webpack 的几个插件,希望对有相同需求的朋友有所帮助。

欢迎指正!



原文发布时间为:2018年06月25日
原文作者:其言

本文来源: 掘金 如需转载请联系原作者




相关文章
|
前端开发 JavaScript
React项目路由懒加载lazy、Suspense,使第一次打开项目页面变快
本文介绍了在React项目中实现路由懒加载的方法,使用React提供的`lazy`和`Suspense`来优化项目首次加载的速度。通过将路由组件改为懒加载的方式,可以显著减少初始包的大小,从而加快首次加载速度。文章还展示了如何使用`Suspense`组件包裹`Switch`来实现懒加载过程中的fallback效果,并提供了使用前后的加载时间对比,说明了懒加载对性能的提升作用。
1086 2
React项目路由懒加载lazy、Suspense,使第一次打开项目页面变快
|
运维 前端开发 数据可视化
【CodeBuddy】挑战一句话开发一个完整项目之:React表单验证系统
本文分享了一个基于React 19构建的用户注册表单系统,采用模块化CSS和状态驱动视图更新,实现实时校验、错误提示与提交反馈等功能。核心亮点包括验证规则引擎(如密码复杂度校验)、交互反馈体系(输入框警示、按钮禁用)及加载动画优化。通过函数式更新确保状态同步,正则表达式实现多条件验证,CSS伪元素打造流畅体验。代码结构清晰,可扩展性强,适合作为React表单开发模板。文末附CodeBuddy免费下载链接,助力高效开发!
296 1
【CodeBuddy】挑战一句话开发一个完整项目之:React表单验证系统
|
存储 缓存 JavaScript
如何优化React或Vue应用的性能
需要注意的是,性能优化是一个持续的过程,需要根据具体的应用场景和性能问题进行针对性的优化。同时,不同的项目和团队可能有不同的优化重点和方法,要结合实际情况灵活运用这些优化策略,以达到最佳的性能效果。
763 158
|
前端开发 开发者
Webpack 插件底层的实现原理是什么?
Webpack 插件通过其插件系统扩展功能,满足不同构建需求。基于事件流模型,Webpack 在编译过程中触发多种事件(如 compile、make、emit、done),插件可监听并执行自定义逻辑。Webpack 使用 tapable 模块管理这些事件,提供 sync、async、promise 等钩子类型。开发者在配置文件中注册插件,通过 apply 方法初始化并注册所需钩子。插件生命周期与编译过程紧密相关,在不同阶段介入执行任务。例如,compilation 事件在每次编译开始时触发,emit 事件在生成输出文件前触发,done 事件在编译完成时触发。
|
前端开发 UED 开发者
React 选项卡组件 Tabs:从基础到优化
本文详细介绍了如何在React中构建一个功能丰富的选项卡组件,包括基础实现、样式美化、常见问题及解决方法。通过逐步讲解,从简单的选项卡组件结构开始,逐步引入样式、性能优化、动态内容加载、键盘导航支持和动画效果,最后讨论了自定义样式的实现。旨在帮助开发者在React项目中高效构建高质量的选项卡组件。
538 18
|
前端开发 UED
React 文本区域组件 Textarea:深入解析与优化
本文介绍了 React 中 Textarea 组件的基础用法、常见问题及优化方法,包括状态绑定、初始值设置、样式自定义、性能优化和跨浏览器兼容性处理,并提供了代码案例。
492 10
|
移动开发 前端开发
react项目配合diff实现文件对比差异功能
在React项目中,可以使用`diff`库实现文件内容对比差异功能。首先安装`diff`库,然后在组件中引入并使用`Diff.diffChars`或`Diff.diffLines`方法比较文本差异。通过循环遍历`diff`结果,可以生成不同样式的HTML元素来高亮显示文本差异。
912 1
react项目配合diff实现文件对比差异功能
|
前端开发 算法 JavaScript
React项目input输入框输入自动失去焦点
本文讨论了在React项目中如何处理input输入框自动失去焦点的问题,特别是在移动端开发中。文章提供了一个使用React Native的TouchableWithoutFeedback组件来监听点击事件,并在事件处理函数中通过调用Keyboard.dismiss()方法使输入框失去焦点的示例代码。这种方法可以确保在用户点击页面其他区域时,键盘能够收起,输入框失去焦点。
627 1
React项目input输入框输入自动失去焦点
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
测试技术 开发者
如何确保 Webpack plugin 与其他插件的兼容性?
【10月更文挑战第23天】确保 Webpack plugin 与其他插件的兼容性需要从多个方面进行考虑和努力。通过遵循规范、进行充分测试、保持沟通协作等方式,