27.webpack——一文掌握webpack性能优化(高频面试点)

【重学webpack系列——webpack5.0】

1-15节主要讲webpack的使用,当然,建议结合《webpack学完这些就够了》一起学习。
从本节开始,专攻webpack原理,只有深入原理,才能学到webpack设计的精髓,从而将技术点运用到实际项目中。
可以点击上方专栏订阅哦。

以下是本节正文:


webpack打包性能优化

1.优化大纲

weibapck性能优化主要可以考虑以下几方面

  1. 缩小查找范围,加快查找速度
    1. extensions
    2. alias
    3. modules
    4. mainFields
    5. mainFiles
    6. resolve
    7. resolveLoader
  2. noParse 不解析第三方库的第三方依赖
  3. DefinePlugin 创建编译时可以配置的全局变量,减少变量查找范围
  4. IgnorePlugin webpack 忽略某些模块,不把他们打包进去
  5. 净化css,抽离css
  6. thread-loader代替happypack,多进程处理
  7. cdn
  8. tree Shaking
  9. 代码分割
  10. 按需加载、提取公共代码
  11. scope Hosting 作用域提升
  12. 利用缓存
    1. babel-loader开启缓存
    2. 使用cache-loader

性能分析工具

  1. speed-measure-webpack-plugin 测试每个核心步骤耗费的事件
  2. webpack-bundle-analyzer 代码分析工具,生成代码体积等分析报告

2.优化详细说明

1.缩小查找范围,加快查找速度
1.1 extensions

有了这个extensions后,在requireimport的时候不需要加文件扩展名,会一次添加扩展名进行匹配

resolve: {
  extensions: [".js",".jsx",".json",".css"]
},
1.2 alias

配置别名可以加快webpack查找模块的速度

不需要从node_modules文件夹中按模块的查找规则查找

const bootstrap = path.resolve(__dirname,'node_modules/_bootstrap@3.3.7@bootstrap/dist/css/bootstrap.css');
resolve: {
    alias:{
        "bootstrap":bootstrap
    }
},
1.3 modules

modules 字段指定第三方模块的查找目录

// 默认是查找node_modules,但是会类似Nodejs一样的路径进行搜索,一层一层网上找node_modules
resolve: {
	modules: ['node_modules'],// 先当当前目录下的node_modules,找不到找上层目录的node_moudles,直到全局的node_modules
}
// 如果确定依赖模块在项目根目录下的node_modules中,那么可以写绝对路径确定查找范围
resolve: {
	modules: [path.resolve(__dirname, 'node_modules')], // 确定查找目录就是项目下的node_modules,找不到不会往上层找
}    
1.4 mainFilds和mainFiles
resolve: {
  // 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
  mainFields: ['browser', 'module', 'main'],
  // target 的值为其他时,mainFields 默认值为:
  mainFields: ["module", "main"],
}
  • 这里的mainFileds代表了一个包解析入口文件应该看的字段,按照上面代码的顺序查找
resolve: {
  mainFiles: ['index'], // 你可以添加其他默认使用的文件名
},
  • 这里的mainFiles代表入口文件,如果mainFileds对应的字段没有,那么就看mainFiles对应的入口文件,如果mainFiles也没有,那么就是index
问:解析一个包或模块,如何找到入口文件面试点
  1. 先找到包/模块下对应的package.json中的main字段,如果存在此字段,直接找到了,就直接返回。
  2. 如果没有这个字段,就找对应mainFiles,默认就是index.js
1.5 resolveLoader

用于配置解析loader时的resolve,默认配置:

module.exports = {
  resolveLoader: {
    modules: [ 'node_modules' ],
    extensions: [ '.js', '.json' ],
    mainFields: [ 'loader', 'main' ]
  }
};

resolve与resolveLoader对象的key是一样的,后者是专门用于查找loader的

2.noParse 忽略对指定第三方模块的第三方依赖的解析

比如:

import jq from 'jquery'

当解析jq的时候,会去解析jq这个库是否有依赖其他的包

但是,如果配置了noParse,那么就不需要再去解析jquery中的依赖库了,这样能够增加打包速率。

module:{
    noParse:/jquery/,//不去解析jquery中的依赖库
    rules: [
        ...
    ]
}
3.DefinePlugin 定义全局变量

创建一些在编译时可以配置的全局变量,在编译的时候会直接替换掉,不需要再去查找

let webpack = require('webpack');
new webpack.DefinePlugin({
    PRODUCTION: JSON.stringify(true),
    VERSION: "1",
    EXPRESSION: "1+2",
    COPYRIGHT: {
        AUTHOR: JSON.stringify("yh")
    }
})

console.log(PRODUCTION);
console.log(VERSION);
console.log(EXPRESSION);
console.log(COPYRIGHT);
  • 如果配置的值是字符串,那么整个字符串会被当成代码片段来执行,其结果作为最终变量的值
  • 如果配置的值不是字符串,也不是一个对象字面量,那么该值会被转为一个字符串,如 true,最后的结果是 ‘true’
  • 如果配置的是一个对象字面量,那么该对象的所有 key 会以同样的方式去定义
  • JSON.stringify(true) 的结果是 ‘true’
4.IgnorePlugin 忽略某些模块,不把他们打包进去

忽略第三方包的指定目录,让这些指定目录不要被打包进去。比如moment包,我只用中文的,那么可以用这个插件指定只打包中文的目录,其他语言就不需要打包了。加快了打包速度,减少了打包体积。

18、webpack优化(3)——IgnorePlugin_俞华的博客-CSDN博客_ignoreplugin

5.purgecss-webpack-plugin净化css,mini-css-extract-plugin压缩并抽离css

purgecss-webpack-plugin净化css,mini-css-extract-plugin压缩并抽离css,两者需要配合使用

  • 使用:
const path = require("path");
+const glob = require("glob");
+const PurgecssPlugin = require("purgecss-webpack-plugin");
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PATHS = {
  src: path.join(__dirname, 'src')
}
module.exports = {
  mode: "development",
  entry: "./src/index.js",
  module: {
    rules: [
      {
        test: /\.js/,
        include: path.resolve(__dirname, "src"),
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env", "@babel/preset-react"],
            },
          },
        ],
      },
+      {
+        test: /\.css$/,
+        include: path.resolve(__dirname, "src"),
+        exclude: /node_modules/,
+        use: [
+          {
+            loader: MiniCssExtractPlugin.loader,
+          },
+          "css-loader",
+        ],
+      },
    ],
  },
  plugins: [
+    new MiniCssExtractPlugin({
+      filename: "[name].css",
+    }),
+    new PurgecssPlugin({
+      paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true }),
+    })
  ],
};
6.thread-loader代替happypack,多进程处理

把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  module: {
    rules: [
      {
        test: /\.js/,
        include: path.resolve(__dirname, "src"),
        use: [
+          {
+            loader:'thread-loader',
+            options:{
+              workers:3
+            }
+          },
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env", "@babel/preset-react"],
            },
          },
        ],
      },
      {
        test: /\.css$/,
        include: path.resolve(__dirname, "src"),
        exclude: /node_modules/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          "css-loader",
        ],
      },
    ],
  }
};
7.CDN 内容分发网络

CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。

问:怎么使用CDN提高性能,加快访问速度?面试题
  • 首先,我们一般将入口html文件不缓存,放在自己的服务器上,关闭自己服务器的缓存,静态资源的url变成指向CDN服务器的地址
    • 这样做,是因为cdn服务一般会给资源开启很长时间,例如用户从cdn上获取了index.html后,即使之后发布操作把index.html覆盖了,但是用户在很长一段时间内,依旧使用的是之前运行的版本,这会导致新发布不能立即生效,所以我们将index.html不开启缓存放在自己服务器上
  • 然后,很对静态的js、css、图片等文件需要开启缓存,上传到CDN上去,并且给每个文件名带上由文件内容算出的hash值
    • 开启缓存和上传到CDN是为了能够读取更快,加上hash是因为文件会随着内容而变化,只要文件内容变化,那么对应的url就会变化,那么就会重新下载,无论缓存时间有多长。这样能保证文件一更新,读取的就是最新的文件,文件不更新,读取的就是缓存。
  • 启用CDN后,所有的相对路径都改成指向CDN服务器的绝对路径。通过webpack的publicPath可以设置。
{
        output: {
        path: path.resolve(__dirname, 'dist'),
+       filename: '[name]_[hash:8].js',
+       publicPath: 'http://img.aiqiyi.cn'
    },
}
  • 另外,会把不同的静态资源分散到不同的CDN服务上去,因为同一时候,针对同一域名的资源 并行请求是优先的。

  • 但是,多个域名会增加域名解析时间,所以可以在HTML的HEAD标签中加入link标签去与解析域名,以降低域名解析带来的延迟。

    • link标签一般会有一个rel属性,值为dns-prefetch,代表dns预拉取,拉取的地址是href属性的值,比如:

      <link rel="dns-prefetch" href="http://img.aiqiyi.cn">
      
8.TreeShaking 只打包用到的API

tree shaking就是只把用到的方法打入bundle,没用到的方法会uglify阶段擦除掉

  • 原理是利用es6模块的特点,只能作为模块顶层语句出现,import的模块名只能是字符串常量,所以只能用在esModule中面试点

    • 没有导入的API都不会被打包

    • 代码不会被执行,不可到达的不会被打包

      import a from 'a'if(false){	console.log(a) // 不会达到,所以a不会打包进来}
      
    • 代码执行的结果不被用到,不会被打包

    • 代码中只赋值,不使用的变量,不会被打包

9.代码分割
9.1 按入口点分割

多入口项目,入口chunks之间包含了重复的模块,那么这些重复的模块会被引入到各个bundle中,所以提取出重复的模块作为单独的bundle,能够提升减少打包体积,提升打包效率

9.2动态导入(懒加载,按需加载)

使用import()函数配合魔法注释,代表按需加载,魔法注释一样的会打成一个包

import(/* webpackChunkName: "title" */ "./components/Title")
9.3预先加载preload和预先拉取prefetch
9.3.1 预先加载preLoad
  • preload-webpack-plugin 能够将资源加载通过preload的方式加载,也就是预先加载
    • 举例如下:
      • 现在需要preload的地方,添加魔法注释"/* webpackPreload: true*/"(图1-1)
      • 然后webpack配置插件(图1-2)
      • 然后会发现,原先按需加载的权重应该是Low,但是现在变成了Hight(图1-3)
      • 从html发现,还没点击按钮,已经插入了link标签,也就是资源已经被预先加载了(图1-4)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BjvRDjCw-1629171039573)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210816135829395.png)]

​ (图1-1)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z5vloiCC-1629171039579)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210816140132788.png)]

​ (图1-2)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kCFq7pWC-1629171039582)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210816140241992.png)]

(图1-3)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Yw6ogfC-1629171039584)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210816140214366.png)]

(图1-4)

  • 上面的html可以看出,link标签不止可以引出css,还可以引入script,只要写一个as=“script”,应该还可以引入其他类型。另外,上面的rel="preload"表示预加载
9.3.2预先拉取prefetch
  • prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源
<link rel="prefetch" href="utils.js" as="script">
button.addEventListener('click', () => {
  import(
    `./utils.js`
    /* webpackPrefetch: true */
    /* webpackChunkName: "utils" */
  ).then(result => {
    result.default.log('hello');
  })
});
9.3.3 preload 与 prefetch 的区别
  • preload告诉浏览器我马上要用到这个资源,请提高加载优先级为high,预先加载

  • prefetch告诉浏览器我未来可能会用到这个资源,请浏览器优先级设为lowest,在浏览器空闲的时候加载

    • preload不要轻易用,因为会阻塞主要模块的加载,所以慎用。只有最关键最急需的资源才用。
9.4 提取公共代码
  • 问:以下模块依赖图,由page1、page2、page3三个入口,实线代表依赖,比如page1依赖了module1,虚线代表动态导入,比如page1动态导入asyncModule1.js。请问:利用你觉得最佳的优化方案,会产出哪些文件?

    • 思路:

      1. 提取第三方模块 jquery

        会提取出一个endorspage1page2~page3.chunk.js

      2. 提取公共模块 module1 module2 module3

        会提取出page1page2.chunk.js、page1page2~page3.chunk.js

      3. 提取动态加载的模块(import()),动态模块也按照1 2 3步骤提取

        会提取出asyncModule1.chunk.js,然后还会提取出动态加载的模块的第三方依赖vendors~asyncModule.chunk.js,然后去提供公共模块,因为这里没有,所以么有产出

      4. 提取出入口文件

        会提取出page1.js、page2.js、page3.js

  • 问:module3在哪个产出文件中?

    • 答:在page3.js中。因为如果一个模块被两个或两个以上引用,那么会单独打包出一个bundle,如果只有被一个引用,那么就会打包到引用方的包中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nxseuTvw-1629171039586)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210816201901360.png)]

  • 分包是什么意思?

    分包就是提取多个入口之间的公共模块,一般用于mpa多页应用

    那么单页应用如何分包?通过懒加载import实现分包

  • 代码分割的配置应该怎么配?

    应该在webpack配置文件的optimization属性的splitChunks属性中配置

module.exports = {
   optimization: {
     splitChunks: {
         chunks: "all", //代码分割应用于哪些情况,默认作用于异步chunk,值为all(同步+异步)/initial(同步import a from './a')/async(异步import())
         minSize: 0, //分割出去的代码块最小的尺寸多大,代码块的最小尺寸,小于这个尺寸的就没必要分割出来,太小了,默认值是30kb,0就是不限制
         minChunks: 2, //被多少模块共享,在分割之前模块的被引用次数
         maxAsyncRequests: 3, //限制异步模块内部的并行最大请求数的,说白了你可以理解为是每个import()它里面的最大并行请求数量
         maxInitialRequests: 5, //限制入口的拆分数量
         name: false, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~,值可以是false、string或function,不能是true
         automaticNameDelimiter: "~", //分隔符,默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
         cacheGroups: {// 将多个chunk的缓存合并成一个组
           //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
           vendors: {
             chunks: "all",
             test: /node_modules/, //条件,如果这个模块request路径(绝对路径)里面包含node_modules,那么就属于第三方模块
             priority: -10, ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
           },
           default: {
             chunks: "all",
             minSize: 0, //最小提取字节数
             minChunks: 2, //最少被几个chunk引用
             priority: -20,
             reuseExistingChunk: false
           }
           // 有些模块,比如jquery,按照上面的配置,它会同时属于vendors和default两个缓存组,那么到底属于哪个缓存组呢,根据配置中的priority值大小来确定。
         },
         runtimeChunk:true // 运行时代码块单独打包成一个文件
   },
}
  • 上面的缓存组中,为什么priority要配置成负数?

    • 答:因为webpack有默认缓存组,默认缓存组优先级为0。
      • 如果你想要优先级比默认的要高,用正数
      • 比默认的要低,就用负数。(负数的话,就是不覆盖默认配置)
  • 多页应用html怎么配置?

    • 根据下图配置,但是需要配置chunks,代表page1.html引用对应自己的chunks。
10.scope Hosting 作用域提升

Scope Hosting作用于提升,可以让 Webpack 打包出来的代码体积更小、运行速度更快。这是webpack3退出的功能。

  • 为什么Scope Hosting能够减少代码体积、加快运行速度?

    答:举个例子说明下:

    如果文件a导出了字符串a,index.js引入了a,并且console.log(字符串a),那么如果没有开启scope hosting,打包出来的结果是:

    在modules数组中,有一个对象元素,key为a模块路径,value为一个函数,函数内部包含了a模块的内容;然后还有一个index.js模块,根据引用关系去找到modules中的a模块,然后加载进来。相当于打包后,a模块的内容都被打包进去了。

    但其实,我只是在index.js中使用了一下a模块的导出结果,也就是字符串a,那么我只要把index.js中用到a的地方替换成字符串’a’就可以了,没必要把整个a模块都打包进来。scope hosting就是用来做到这一点的。

// a.js
export default 'a';
// index.js
import a from './a.js';
console.log(a);
// 不开启scope hosting打包出来的结果
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var a = ('a');
console.log(a);
})
// 开启scope hosting打包出来的结果
(() => {"use strict";console.log("a")})
11.利用缓存

webpack利用缓存来优化一般有2种思路:

  • babel-loader开启缓存
  • 使用cache-loader
11.1babel-loader
  • Babel在转义js文件过程中消耗性能较高(语法树解析啥的),将babel-loader执行的结果缓存起来,当重新打包构建时会尝试读取缓存,从而提高打包构建速度、降低消耗
 {
    test: /\.js$/,
    exclude: /node_modules/,
    use: [{
      loader: "babel-loader",
      options: {
        cacheDirectory: true
      }
    }]
  },
11.2cache-loader
  • 在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里
  • 存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader
const loaders = ['babel-loader'];
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'cache-loader',
          ...loaders
        ],
        include: path.resolve('src')
      }
    ]
  }
}
12.oneOf

一般来说,一个类型的文件只对应一个rule,但是webpack编译的时候,会对每个rules中的所有规则都遍历一遍,匹配test规则的整理出来,然后去执行laoder。

但是,如果用了oneOf,只要匹配test,匹配到一个,那么后面的loader就不再处理了。

也正是一个只匹配一个,所以oneOf中不能两个配置处理同一种类型的文件。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        //优先执行
        enforce: 'pre',
        loader: 'eslint-loader',
        options: {
          fix: true
        }
      },
      {
        // 以下 loader 只会匹配一个
        oneOf: [
          ...,
          {test: /js/, ...}, // 匹配到一个,后面就不会再去匹配了
          {test: /css/, ...}
        ]
      }
    ]
  }
}

3.性能分析工具

1. speed-measure-webpack-plugin 测试每个核心步骤耗费的时间
  • 使用:在module.exports导出的内容外包一层wrap函数即可。
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
module.exports =smw.wrap({
});
  • 结果:可以看到每个步骤、每个loader、plugin等消耗的时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yc8Yri4u-1629171039588)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210817111627463.png)]

2.webpack-bundle-analyzer

webpack-bundle-analyzer是一个webpack的插件,需要配合webpack和webpack-cli一起使用。这个插件的功能是生成代码分析报告,帮助提升代码质量和网站性能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iPWd01JB-1629171039589)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210816213421749.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iRfEQ2YR-1629171039589)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210813102850017.png)]

上面这个插件的用法改了…

耗时分析

const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');const smw = new SpeedMeasureWebpackPlugin();module.exports =smw.wrap({    ...});
  • 可以看到每个步骤、每个loader、plugin等消耗的时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JP3aEcTB-1629171039590)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210813135009488.png)]

webpack打包文件分析工具webpack-bundle-analyzer

webpack-bundle-analyzer是一个webpack的插件,需要配合webpack和webpack-cli一起使用。这个插件的功能是生成代码分析报告,帮助提升代码质量和网站性能

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports={
  plugins: [
    new BundleAnalyzerPlugin()  // 使用默认配置
    // 默认配置的具体配置项
    // new BundleAnalyzerPlugin({
    //   analyzerMode: 'server',
    //   analyzerHost: '127.0.0.1',
    //   analyzerPort: '8888',
    //   reportFilename: 'report.html',
    //   defaultSizes: 'parsed',
    //   openAnalyzer: true,
    //   generateStatsFile: false,
    //   statsFilename: 'stats.json',
    //   statsOptions: null,
    //   excludeAssets: null,
    //   logLevel: info
    // })
  ]
}
// 配置脚本
{
 "scripts": {
    "generateAnalyzFile": "webpack --profile --json > stats.json", // 生成分析文件
    "analyz": "webpack-bundle-analyzer --port 8888 ./dist/stats.json" // 启动展示打包报告的http服务器
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值