zaishiyu

前言

  本文来总结写webpack 在性能方面常见的优化方案。

正文

  本文分别总结开发环境和生产环境中在打包构建速度和代码调试功能方面的优化方案,如下:

  1、开发环境性能优化

  (1)优化打包构建速度

  a、HMR: hot module replacement ,热模块替换,作用:当一个模块发生变化的时候,只会重新打包发生变化的模块,并不会打包所有模块,极大的提升了代码构建速度。

  只需要在webpack.config.js中的devserver中添加:

  devServer: {
    // contentBase: resolve(__dirname, "build"),// 这个配置在新版本的webpack中使用下面的方式配置,否则会报错
    static: { // static: ['assets']
      directory: resolve(__dirname, "build")
    },
    // 启用gzip压缩
    compress: true,
    // 端口号
    port: 3000,
    open:true,// 自动打开浏览器
    hot:true// 开启HMR功能
  },

  执行 npx webpack-dev-server命令,然后修改其中的文件,会发现控制台提示修改的文件,如下:

  经测试发现总结:

  样式文件:可以使用HRM功能,因为style-loader 内部实现了

  js文件:默认不能使用HRM功能,(解决方法:修改js代码,添加支持)

  html 文件:默认不能使用HRM 功能,同时会导致html文件不能热更新了(解决方法:修改入口文件entry的引入即可,如下:)开发中不需要做

    //入口文件
      entry: ["./src/index.js","./src/index.html"],

  (2)优化代码调试性能

  source-map:是一种提供源代码到构建后代码的映射技术(如果构建后代码出错,通过映射关系能够追溯到源代码错误)

  只需要在webpack.config.js中添加如下:

    devtool:"eval-source-map"

  参数:[ inline -|hidden-| eval- ] [ nosources- ] [cheap- [module-]] source-map

  source-map:默认外部,能够准确提示文件报错的具体位置和准确信息

  inline-source-map:内联。只生成一个内联的source-map,能够准确提示文件报错的具体位置和准确信息。但是体积较大

  hidden-source-map:外部,提示错误代码信息,不能追踪到源代码错误位置,只能提示到构建后代码错误位置。

  eval-source-map:内联,每个文件都会生成对应的source-map ,都在eval(),能够准确提示文件报错的具体位置和准确信息,不过多了源代码文件名加了hash值。

  nosources-source-map:外部,能够找到错误信息,但是没有任何源代码错误位置信息。

  cheap-source-map:外部,能够准确提示文件报错的具体位置和准确信息,只能精确到代码行。

  cheap-module-source-map:外部,能够准确提示文件报错的具体位置和准确信息,只能精确到代码行。但是module会将loader的 source-map 加入进来

  内联和外部的区别:外部生成了文件,而内联没有,内联构建速度更快。

  使用方法: 开发环境:速度快优先,便于调试( eval > inline > cheap > 其他),因此 eval-source-map最优

       生产环境:源代码要不要隐藏,调试要不要友好,nosources-source-map 或者 hidden-source-map

  2、生产环境性能优化

  (1)优化打包构建速度

  (2)优化代码调试性能

  a、oneOf:在loader中配置了很多个用于处理不同的文件,但是针对不同文件不能全部使用多个loader,只能使用其中一个或者几个,每次使用不需要都对loader检查一遍,因此可以对loader中代码进行改造。由于对js文件即使用了eslint-loader 又使用了babel-loader,所有把eslint-loader 提取到外部即可。如下:

 module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/, // 排除node_modules
        enforce: "pre", // 优先执行,先检查eslint 再执行babel-loader
        loader: "eslint-loader",
        options: {
          fix: true, // 自动修复eslint错误
        },
      },
      {
        oneOf: [
          {
            test: /\.css$/,
            use: [
              // "style-loader", // 创建style标签将样式放入
              //这个loader取代style-loader ,作用是将提取js中的css成单独的文件
              MiniCssExtractPlugin.loader,
              "css-loader", // 将css文件整个到js中
              /* CSS兼容性处理:postcss ==>postcss-loader postcss-preset-env
            帮助postcss扎到packae.json 中browserslist里的配置,通过配置加载指定的css兼容样式
            "browserlist":{
              // 开发环境==》设置node环境变量:process.env.NODE_ENV="development"
                "development":[
                  "last 1 chrome version",
                  "last 1 firefox version",
                  "last 1 safari version"
                ],
                // 生产环境 默认是生产环境
                "production":[
                  ">0.2%",
                  "not dead",
                  "not op_mini all"
                ]
              }*/
              // 使用loader的默认配置
              // post-loader
              // 修改loader配置
              {
                loader: "postcss-loader",
                // options:{
                // ident:"postcss",
                // plugins:()=>{
                //   require("postcss-preset-env")
                // }
                // }
              },
            ],
          },

          {
            test: /\.js$/,
            loader: "babel-loader",
            exclude: /node_modules/, // 排除node_modules
            options: {
              // 预设:指示babel做哪些兼容性处理
              presets: [
                // "@babel/presets-env",
                {
                  useBuiltins: "usage", // 按需加载
                  corejs: {
                    version: 3, // 指定corejs版本
                  },
                  targets: {
                    // 指定兼容到哪些浏览器
                    chorme: "60",
                    firefox: "60",
                    ie: "9",
                    safiri: "10",
                    edge: "17",
                  },
                },
              ],
            },
          },
          // 这种方式无法处理html文件中的图片
          {
            test: /\.(jpg|png|gif)$/,
            loader: "url-loader",
            options: {
              // 图片大小小于8kb,会被解析为base64处理,优点减少请求数量减轻服务器压力,缺点是图片体积更大,文件请求速度更慢
              limit: 8 * 1024,
              esModule: false,
              // 这样可以修改图片名称,[hash:10]表示取hash值的前十位,[ext]表示原文件扩展名
              name: "img/[hash:8].[name].[ext]",
            },
          },
          {
            // 因为url-loader默认适用了es6模块解析,而html-loader 引入图片适用commonJS处理,解析时会出现[object Module],需要关闭url-loader的es6模块化解析
            test: /\.html$/,
            // 处理html 文件的img文件
            loader: "html-loader",
          },
          {
            // 打包其他资源(除了css,js,html,less,json资源以外的资源)
            exclude: /\.(css|js|html|less|json)$/,
            loader: "file-loader",
          },
        ],
      },
    ],
  },

  b、缓存优化

  <1>babel缓存优化

  babel缓存指在通过babel实现 js 兼容性处理的时候,默认我们每次会对所有的js代码进行babel处理,而实际构建过程中,我们并不hi修改全部js的代码,因此只需要对我们修改的js代码进行babel兼容性处理即可,没有修改的可以使用缓存的处理就行。

  只需要在babel的配置中添加配置,如下即可:

         {
            test: /\.js$/,
            loader: "babel-loader",
            exclude: /node_modules/, // 排除node_modules
            options: {
              //... 其他处理
              // 开启babel缓存,第二次构建时,会读取之前的缓存
              cacheDirectory: true,
            },
          },

  <2>打文件资源的优化

  给生成的文件添加hash值(webpack每次打包生成的唯一值),每次通过对比hash值的变化来确定是否使用缓存的文件。但是问题是:因为js和css同时使用一个hash值,重新打包,会导致所有缓存失效,无法满足缓存使用。

  给生成的文件添加 chunkhash :根据生成的hash值,如果打包来源于同一个chunk,那么hash值就一样。但是问题是:因为css是在js中引入的,所有同属于一个chunk。

  给生成的文件添加contenthash :根据文件的内容生成的hash值,不同文件hash值一定不一样。只需要在filename中添加如下代码:

 output: {
    // 输出文件名
    filename: "js/build.[contenthash:10]js",
    // 输出路径
    // __dirname 是node.js的变量,代表当前文件的目录的绝对路径
    path: resolve(__dirname, "build"), // 代表输出到当前目录下创建的build文件夹下
  },

  c、tree shaking:去除无用代码(js和css)

  前提:1、使用es6 模块化 2、开启production环境

  作用:减少打包代码体积

  需要在package.json 中配置如下:这样可以把一些没有使用的副作用文件保存在构建包中,比如没有css,less文件

  "sideEffects":[
    "*.css",
    "*.less"
  ]

  d、code split代码分割

  在分割之前,js文件是打包成为一个文件,然后在html中引入,无法实现我们的按需加载,对性能影响很大。

  (1)设置多入口,只要有一个入口,最终就会输出一个bundle ,通过拆分入口实现生成多个bundle

  entry:{
    main:"./src/js/index.js",
    test:"./src/js/index.js"
  },

  (2)在webpack.config.js中添加如下配置,对项目依赖单独打包

  // 可以将node_modules中代码单独打包一个chunk输出
  // 自动分析多个入口chunk中,有没有公共的文件,如果有会打包成一个单独的chunk
  optimization:{
    splitChunks:{
      chunks:"all"
      }
  },

  不使用如上配置,当我们的代码中A.js 和 B.js 两个文件同时引入了 jquery 依赖的时候,都会生成对应的AB打包的文件,这两个文件中就会重复存在jquery的打包文件,这时就需要单独分离出依赖文件,单独进行打包即可。

  (3)如果不想设置多个入口文件,可以通过js代码将某个文件单打包成一个chunk

import (/*webpackChunkName:"test"*/"./test.js")
.then((res)=>{
  // 文件加载成功
  console.log(res);
})
.catch(()=>{
  console.log("文件加载失败");
})

  通过上面的 import动态导入语法, test.js 文件就会被单独打包

  e、懒加载和预加载

  懒加载是使用上面提到的import动态导入语法,将代码放在一个异步的函数中,如下:

document.getElementById("btn").onclick = function () {
  import(/*webpackChunkName:"test"*/ "./test.js")
    .then((res) => {
      // 文件加载成功
      console.log(res);
    })
    .catch(() => {
      console.log("文件加载失败");
    });
};

  在index.js 文件中,点击btn按钮,进行test.js 文件 的懒加载,再次点击的时候,不会重复加载test文件,此时会利用缓存里的test文件即可。

  预加载prefetch,会在使用之前提前加载js文件,在使用的时候直接在缓存里面取,代码如下:

document.getElementById("btn").onclick = function () {
  import(/*webpackChunkName:"test",webpackPrefetch:true*/ "./test.js")
    .then((res) => {
      // 文件加载成功
      console.log(res);
    })
    .catch(() => {
      console.log("文件加载失败");
    });
};

  正常加载可以认为是并行加载,同一时间加载多个文件,预加载prefetch是等其他资源加载完毕,浏览器空闲的时候取加载其他资源。极大提高性能和用户体验。但是预加载兼容性较差,谨慎使用。

  f、PWA (Progressive Web App),即渐进式WEB应用。

  • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏

  • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能

  • 实现了消息推送

    使用workbox技术 ===》workbox-webpack-plugin配置插件

    首先下载workbox-webpack-plugin库

npm i workbox-webpack-plugin -D

  在plugins中添加配置

   new WorkboxWebpackPlugin.GenerateSW({
      /**
       * 1.帮助serviceworker快速启动
       * 2.删除旧的serviceworker
       * 
       * 生成一个serviceworker配置文件~
       */
      clientsClaim:true,
      skipWaiting:true
    })

  在入口文件或者html文件中可以添加如下代码注册serviceworker

// 注册serviceworker
// 处理兼容性问题
/**
 * 1、eslint不能识别window.navigator这些变量,需要加package.json中的eslintConfig中配置
 *            "eslintConfig": {
                    "extends": "airbnb-base",
                    "env":{
                    "browser": true
                    }
                },
    2、 serviceworker 代码必须运行在服务器上
                ==》通过node.js部署
                ==》npm i serve -g 安装serve,然后serve -s build启动服务器,将build目录下所有资源作为静态资源暴露出去
 */
if("serviceWorker" in navigator){
    window.addEventListener("load",()=>{
        navigator.serviceWorker.register("/service-worker.js")// 该js文件由插件生成
        .then(()=>{
            console.log("注册成功");
        })
        .catch(()=>{
            console.log("注册失败");
        })
    })
}

  g、thread-loader多进程打包

  安装loader:

npm i thread-loader -D

  经常给babel-loader使用:

         {
            test: /\.js$/,
            exclude: /node_modules/, // 排除node_modules
            use: [
              {
                // 开启多进程打包 进程开启大概600ms,进程通信也要花时间开销
                loader:"thread-loader",
                options:{
                  workers:2,// 两个进程打包
                }
              },
              {
                loader: "babel-loader",
                options: {
                  // 预设:指示babel做哪些兼容性处理
                  presets: [
                    // "@babel/presets-env",
                    {
                      useBuiltins: "usage", // 按需加载
                      corejs: {
                        version: 3, // 指定corejs版本
                      },
                      targets: {
                        // 指定兼容到哪些浏览器
                        chorme: "60",
                        firefox: "60",
                        ie: "9",
                        safiri: "10",
                        edge: "17",
                      },
                    },
                  ],
                  // 开启babel缓存,第二次构建时,会读取之前的缓存
                  cacheDirectory: true,
                },
              },
            ],
          },

  h、externals

  在webpack.cinfig.js 添加入下中配置

  externals:{
    // 拒绝jQuery 被打包进来
    jquery:"jQuery"
  }

  这样jQuery这个包就不会用过npm的方式被打包到项目中,可以通过cdn的方式引入,比如在index.html 文件中插入jQuery 的script标签。通过cdn的引入,提升性能

  i、dll 对代码库进行单独打包

  创建单独的js文件,比如webpack.dll.js

/**
 * 使用dll技术,对某些(第三方库:jquery、react、vue...)进行单独打包,同时生成一个manifest.json文件,提供了之间的映射关系
 * 当运行webpack指令 默认运行的是webpack.config.js配置文件,需要通过webpack --config webpack.dll.js 指令运行webpack.dll.js配置文件
 */
const { resolve } = require("path");
const webpack = require("webpakck");

module.exports = {
  entry: {
    // 最终打包生成的[name] ==>jquery
    jquery: ["jquery"],
  },
  output: {
    filename: "[name].js",
    path: resolve(__dirname, "dll"),
    library: "[name]_[hash]", // 打包的库里面向外暴露出去的内容叫什么名字
  },
  plugins: [
    // 打包生成一个manifest.json 文件(该文件提供映射关系)
    new webpack.DllPlugin({
      name: "[name]_[hash]", // 映射库的暴露的内容名称
      path: resolve(__dirname, "dll/manifest.json"), // 输出文件路径
    }),
  ],
  mode: "production",
};

  通过上面的指令,会将jquery单独进行打包。

  然后在webpack.config.js 中引入新的插件,告诉webpack不去打包哪些依赖(忽略jquery依赖)

  plugins: [
    // 告诉webpack那些库不参与打包,同时使用时的名称也得改变
    new webpack.DllPlugin({
      manifest:resolve(__dirname,"dll/mainfest.json")
    }),
  ],

  然后执行下面命令下载add-asset-html-webpack-plugin

npm i add-asset-html-webpack-plugin -D

  继续如下配置(再将之前dll.js 文件打包的jquery引入进来):

  plugins: [
    // 将某个文件打包输出,并在html中自动引入该资源
    new AddAssetHtmlWebpackPlugin({
      filepath:resolve(__dirname,"dll/jquery.js")
    })
  ],

  通过上面的配置优化了jquery重复打包的过程提高了性能。

写在最后

  以上就是本文的全部内容,希望给读者带来些许的帮助和进步,方便的话点个关注,小白的成长之路会持续更新一些工作中常见的问题和技术点。

 

相关文章: