1.压缩代码

压缩css和压缩js是自带的,只有生产环境才会使用
压缩js
image.png
压缩css
image.png
压缩图片:

  1. {
  2. test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
  3. use: [
  4. "file-loader",
  5. {
  6. + loader: "image-webpack-loader",
  7. + options: {
  8. + mozjpeg: {
  9. + progressive: true,
  10. + quality: 65,
  11. + },
  12. + optipng: {
  13. + enabled: false,
  14. + },
  15. + pngquant: {
  16. + quality: "65-90",
  17. + speed: 4,
  18. + },
  19. + gifsicle: {
  20. + interlaced: false,
  21. + },
  22. + webp: {
  23. + quality: 75,
  24. + }
  25. + }
  26. + }
  27. ]
  28. }

2.purgecss-webpack-plugin

可以去除未使用的css,一般与glob和glob-cli配合使用
必须和mini-css-extract-plugin(不只是压缩作用,可以实现提取css文件内容到单独的文件中去)配合使用
paths路径是绝对路径

  1. npm i purgecss-webpack-plugin mini-css-extract-plugin css-loader glob -D

webpack.config.js

  1. const path = require("path");
  2. +const glob = require("glob");// 按照文件匹配模式匹配文件列表
  3. +const PurgecssPlugin = require("purgecss-webpack-plugin");
  4. +const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  5. const PATHS = {
  6. src: path.join(__dirname, 'src')
  7. }
  8. module.exports = {
  9. mode: "development",
  10. entry: "./src/index.js",
  11. module: {
  12. rules: [
  13. {
  14. test: /\.js/,
  15. include: path.resolve(__dirname, "src"),
  16. use: [
  17. {
  18. loader: "babel-loader",
  19. options: {
  20. presets: ["@babel/preset-env", "@babel/preset-react"],
  21. },
  22. },
  23. ],
  24. },
  25. + {
  26. + test: /\.css$/,
  27. + include: path.resolve(__dirname, "src"),
  28. + exclude: /node_modules/,
  29. + use: [
  30. + {
  31. + loader: MiniCssExtractPlugin.loader, // 可以提取css样式
  32. + },
  33. + "css-loader",
  34. + ],
  35. + },
  36. ],
  37. },
  38. plugins: [
  39. + new MiniCssExtractPlugin({
  40. + filename: "[name].css",
  41. + }),
  42. + new PurgecssPlugin({
  43. + paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
  44. + })
  45. ],
  46. };

3.多进程处理

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

  1. npm i thread-loader- D
  1. const path = require("path");
  2. const glob = require("glob");
  3. const PurgecssPlugin = require("purgecss-webpack-plugin");
  4. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  5. const DllReferencePlugin = require("webpack/lib/DllReferencePlugin.js");
  6. const PATHS = {
  7. src: path.join(__dirname, 'src')
  8. }
  9. module.exports = {
  10. mode: "development",
  11. entry: "./src/index.js",
  12. module: {
  13. rules: [
  14. {
  15. test: /\.js/,
  16. include: path.resolve(__dirname, "src"),
  17. use: [
  18. + {
  19. + loader:'thread-loader',
  20. + options:{
  21. + workers:3
  22. + }
  23. + },
  24. {
  25. loader: "babel-loader",
  26. options: {
  27. presets: ["@babel/preset-env", "@babel/preset-react"],
  28. },
  29. },
  30. ],
  31. },
  32. {
  33. test: /\.css$/,
  34. include: path.resolve(__dirname, "src"),
  35. exclude: /node_modules/,
  36. use: [
  37. {
  38. loader: MiniCssExtractPlugin.loader,
  39. },
  40. "css-loader",
  41. ],
  42. },
  43. ],
  44. },
  45. plugins: [
  46. new MiniCssExtractPlugin({
  47. filename: "[name].css",
  48. }),
  49. new PurgecssPlugin({
  50. paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
  51. }),
  52. new DllReferencePlugin({
  53. manifest: require("./dist/react.manifest.json"),
  54. }),
  55. ],
  56. };

4.Tree Shaking

  • 一个模块可以有多个方法,只要其中某个方法使用到了,则整个文件都会被打到bundle里面去,tree shaking就是只把用到的方法打入bundle,没用到的方法会uglify阶段擦除掉
  • 原理是利用es6模块的特点,只能作为模块顶层语句出现,import的模块名只能是字符串常量

开启:

  • webpack默认支持,在.babelrc里设置module:false即可在production mode下默认开启
  • 还要注意把devtool设置为null 在 package.json 中配置:
  • “sideEffects”: false 所有的代码都没有副作用(都可以进行 tree shaking)
    • 可能会把 css / @babel/polyfill文件干掉 可以设置”sideEffects”:[“*.css”]
  • tree-sharking是在代码压缩合并阶段

⚠️
module:false:不会转化import、export语法,把这个语法给webpack,webpack才能去做进一步的处理
module:true: 转换import 、export变成commonjs 的require 、module.exports,那么webpack就实现不了Tree Shaking
import().then不能实现Tree Shaking
总结:实现Tree Shaking的前提是只能是esmodule,而不是commonjs

为什么commonjs不支持Tree Shaking?
es 静态模块

  1. require (a)
  2. require (b)
  3. /////////////////
  4. let name =await fetch ('./apixxx/xx/')
  5. require(name)
  6. // 打包的时候不知道最终返回的是还是b,动态加载,运行时解析
  7. // import from '不能是变量,固定的一个字符串'
  1. const path = require("path");
  2. const glob = require("glob");
  3. const PurgecssPlugin = require("purgecss-webpack-plugin");
  4. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  5. const PATHS = {
  6. src: path.join(__dirname, 'src')
  7. }
  8. module.exports = {
  9. + mode: "production",
  10. + devtool:false,
  11. entry: {
  12. main: './src/index.js'
  13. },
  14. output:{
  15. path:path.resolve(__dirname,'dist'),
  16. filename:'[name].[hash].js'
  17. },
  18. module: {
  19. rules: [
  20. {
  21. test: /\.js/,
  22. include: path.resolve(__dirname, "src"),
  23. use: [
  24. {
  25. loader:'thread-loader',
  26. options:{
  27. workers:3
  28. }
  29. },
  30. {
  31. loader: "babel-loader",
  32. options: {
  33. //modules":false 不要转化import export
  34. + presets: [["@babel/preset-env",{"modules":false}], "@babel/preset-react"],
  35. },
  36. },
  37. ],
  38. },
  39. {
  40. test: /\.css$/,
  41. include: path.resolve(__dirname, "src"),
  42. exclude: /node_modules/,
  43. use: [
  44. {
  45. loader: MiniCssExtractPlugin.loader,
  46. },
  47. "css-loader",
  48. ],
  49. },
  50. ],
  51. },
  52. plugins: [
  53. new MiniCssExtractPlugin({
  54. filename: "[name].[contenthash].css"
  55. }),
  56. new PurgecssPlugin({
  57. paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
  58. }),
  59. ],
  60. };

functions.js

  1. function func1(){ return 'func1'; }
  2. function func2(){ return 'func2'; }
  3. export { func1, func2 }
  1. import {func2} from './functions';
  2. var result2 = func2();
  3. console.log(result2);
  4. // func1没有导入,则打包的时候func1会被干掉
  1. // 代码执行结果永远不可达,也会被tree sharking
  2. if(false){
  3. console.log('false')
  4. }
  1. var aabbcc='aabbcc';
  2. aabbcc='eeffgg';
  3. //代码中只写不读的变量

告诉 Webpack 你的代码无副作用

  1. // 所有文件都有副作用,全都不可 tree-shaking
  2. {
  3. "sideEffects": true
  4. }
  5. // 没有文件有副作用,全都可以 tree-shaking
  6. {
  7. "sideEffects": false
  8. }
  9. // 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
  10. {
  11. "sideEffects": [
  12. "./src/file1.js",
  13. "./src/file2.js"
  14. ]
  15. }

5.代码分割

在最开始使用Webpack的时候, 都是将所有的js文件全部打包到一个build.js文件中(文件名取决与在webpack.config.js文件中output.filename)

但是在大型项目中, build.js可能过大, 导致页面加载时间过长. 这个时候就需要代码分割

代码分割就是将文件分割成块(chunk), 我们可以定义一些分割点(split point), 根据这些分割点对文件进行分块, 并实现按需加载

5.1 入口点分割

Entry Points:入口文件设置的时候可以配置
这种方法存在的问题

  • 如果入口 chunks 之间包含重复的模块(lodash),那些重复模块都会被引入到各个 bundle 中,对策:提取公共代码
  • 不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码

    1. entry: {
    2. index: "./src/index.js",
    3. login: "./src/login.js"
    4. }

    以上两点中,第一点对我们的示例来说无疑是个问题,因为之前我们在 ./src/index.js 中也引入过 lodash,这样就在两个 bundle 中造成重复引用

    防止重复

    针对以上第一点:配置 dependOn选项,这样可以在多个 chunk 之间共享模块:

    1. const path = require('path');
    2. module.exports = {
    3. mode: 'development',
    4. entry: {
    5. index: {
    6. import: './src/index.js',
    7. dependOn: 'shared',
    8. },
    9. another: {
    10. import: './src/another-module.js',
    11. dependOn: 'shared',
    12. },
    13. shared: 'lodash',
    14. },
    15. output: {
    16. filename: '[name].bundle.js',
    17. path: path.resolve(__dirname, 'dist'),
    18. },
    19. };

    如果我们要在一个 HTML 页面上使用多个入口时,还需设置 optimization.runtimeChunk: ‘single’

    5.2 动态导入和懒加载

  • 用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载 在给单页应用做按需加载优化时
  • 一般采用以下原则:
    • 对网站功能进行划分,每一类一个chunk
    • 对于首次打开页面需要的功能直接加载,尽快展示给用户,某些依赖大量代码的功能点可以按需加载
    • 被分割出去的代码需要一个按需加载的时机

import()语句是天然的代码分割点,如果遇到import()就会分割出一个单独的代码块,单独加载
但是动态加载有个问题就是,如果一个模块比较大,当时真正需要的时候加载比较慢;这个时候用到preload、 prefetch

5.2.1 hello.js

hello.js

  1. module.exports = "hello";

index.js

  1. document.querySelector('#clickBtn').addEventListener('click',() => {
  2. import('./hello').then(result => {// import()语句是天然的代码分割点,如果遇到import()就会分割出一个单独的代码块,单独加载
  3. console.log(result.default);
  4. });
  5. });

index.html

  1. <button id="clickBtn">点我</button>

5.2.2 按需加载

  • 如何在react项目中实现按需加载?

    5.2.2.1 index.js

    index.js ```javascript import React, { Component, Suspense } from “react”; import ReactDOM from “react-dom”; import Loading from “./components/Loading”; /* function lazy(loadFunction) { return class LazyComponent extends React.Component { state = { Comp: null }; componentDidMount() {
    1. loadFunction().then((result) => {
    2. this.setState({ Comp: result.default });
    3. });
    } render() {
    let Comp = this.state.Comp;
    return Comp ? <Comp {...this.props} /> : null;
    
    } }; } / const AppTitle = React.lazy(() => import(/ webpackChunkName: “title” */ “./components/Title”) );

class App extends Component { constructor(){ super(); this.state = {visible:false}; } show(){ this.setState({ visible: true }); }; render() { return ( <> {this.state.visible && ( }> )} </> ); } } ReactDOM.render(, document.querySelector(“#root”));

<a name="ORlr9"></a>
#### 5.2.2.2 Loading.js
src\components\Loading.js
```javascript
import React, { Component, Suspense } from "react";
export default (props) => {
  return <p>Loading</p>;
};

5.2.2.3 Title.js

src\components\Title.js

import React, { Component, Suspense } from "react";
export default props=>{
  return <p>Title</p>;
}

5.2.3 preload 预先加载

  • preload通常用于本页面用到的关键资源,包括js、字体、css文件
  • preload将会把资源的下载权重提高,使得关键数据提前下载好,优化页面打开速度
  • 在资源上添加预先加载的注释,你指明该模块需要立即被使用
  • 一个资源的加载的优先级分为五个级别:
    • Highest 最高
    • High 高
    • Medium 中等
    • Low 低
    • Lowest 最低
  • 异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是Low
  • preload-webpack-plugin

image.png

<link rel="preload" as="script" href="utils.js">
import(
  `./utils.js`
  /* webpackPreload: true */
  /* webpackChunkName: "utils" */
)

5.2.4 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');
    })
    });
    

    5.2.5 preload vs prefetch

  • preload是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源,浏览器会提升加载优先级为hight

  • prefetch是告诉浏览器未来可能需要用到某个资源,浏览器不一定会加载这些资源,浏览器会以一个非常低的优先级lowest,在自己闲着没事干加载这个资源;不需要webpack插件,默认支持
  • 建议:对于页面当前很有必要的资源使用preload(不要轻易用,会阻塞我们浏览器渲染),对于可能在将来页面中的资源使用prefetch ```javascript <!DOCTYPE html>

<a name="x0OA4"></a>
## 5.3 提取公共组件

- [common-chunk-and-vendor-chunk](https://github.com/webpack/webpack/tree/master/examples/common-chunk-and-vendor-chunk)
- [split-chunks-plugin](https://www.webpackjs.com/plugins/split-chunks-plugin)
- 怎么配置单页应用?怎么配置多页应用?
<a name="loCgc"></a>
### 5.3.1 为什么需要提取公共代码
大网站有多个页面,每个页面由于采用相同技术栈和样式代码,会包含很多公共的代码。如果都包含进来会有问题:

- 相同资源被重复加载,浪费用户的流量和服务器成本
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验
- 如果能把公共代码抽离成单独文件进行加载,可以减少网络传输流量,降低服务器成本
<a name="xlBpU"></a>
### 5.3.2 如何提取

- 基础类库,方便长期缓存
- 页面之间的共用代码
- 各个页面单独生成文件
<a name="EFJZA"></a>
### 5.3.3 splitChunks
<a name="d1ShP"></a>
#### 5.3.3.1 module chunk bundle

- module(引入的文件):就是js的模块化webpack支持commonJS、ES6等模块化规范,简单来说就是你通过import语句引入的代码
- chunk: chunk是webpack根据功能拆分出来的,包含三种情况(代码分割原则)
   - 你的项目入口(entry),被多个入口共享,或者来自node_module都会被分割
   - 通过import()动态引入的代码
   - 通过splitChunks拆分出来的代码
- bundle:bundle是webpack打包之后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出

        一个文件对应一个代码块(chunk),一个代码块对应一个bundle
<a name="NIO1F"></a>
#### 5.3.3.2 默认配置
```javascript
optimization: {
        splitChunks: {
            chunks: "all",//表示要分割哪些代码块,值为all=异步+同步/initial同步/async异步
            minSize: 30000,  //默认值是30kb,代码块的最小尺寸
            minChunks: 1,  //被多少模块共享,在分割之前模块的被引用次数
            maxAsyncRequests: 5,  //按需加载最大并行请求数量
            maxInitialRequests: 3,  //一个入口的最大并行请求数量
            name: true,  //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
            automaticNameDelimiter: '~',//默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
            cacheGroups: { //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
                vendors: {
                    chunks: "initial",
                    test: /node_modules/,//条件
                    priority: -10 ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
                },
                commons: {
                    chunks: "initial",
                    minSize: 0,//最小提取字节数
                    minChunks: 2, //最少被几个chunk引用
                    priority: -20,
                    reuseExistingChunk: true//    如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码
                }
            }
        },
}

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
{
  entry: {
    page1: "./src/page1.js",
    page2: "./src/page2.js",
    page3: "./src/page3.js",
  },
 optimization: {
  splitChunks: {
      chunks: "all", //默认作用于异步chunk,值为all/initial/async
      minSize: 0, //默认值是30kb,代码块的最小尺寸
      minChunks: 1, //被多少模块共享,在分割之前模块的被引用次数
      maxAsyncRequests: 3, //限制异步模块内部的并行最大请求数的,说白了你可以理解为是每个import()它里面的最大并行请求数量
      maxInitialRequests: 5, //限制入口的拆分数量
      name: true, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
      automaticNameDelimiter: "~", //默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
      cacheGroups: {
        //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
        vendors: {
          chunks: "all",
          test: /node_modules/, //条件
          priority: -10, ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
        },
        default: {
          chunks: "all",
          minSize: 0, //最小提取字节数
          minChunks: 2, //最少被几个chunk引用
          priority: -20,
          reuseExistingChunk: false
        }
      },
      runtimeChunk:true // 运行时代码块,把运行时提取出去,变成一个chanck
    },
  plugins:[
        new HtmlWebpackPlugin({
                template:'./src/index.html',
                chunks:["page1"],
                filename:'page1.html'
        }),
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            chunks:["page2"],
            filename:'page2.html'
    }),
    new HtmlWebpackPlugin({
            template:'./src/index.html',
            chunks:["page3"],
            filename:'page3.html'
    })
    ]
    }

src\page1.js

import module1 from "./module1";
import module2 from "./module2";
import $ from "jquery";
console.log(module1, module2, $);
import(/* webpackChunkName: "asyncModule1" */ "./asyncModule1");

src\page2.js

import module1 from "./module1";
import module2 from "./module2";
import $ from "jquery";
console.log(module1, module2, $);

src\page3.js

import module1 from "./module1";
import module3 from "./module3";
import $ from "jquery";
console.log(module1, module3, $);

src\module1.js
console.log(“module1”);
src\module2.js
console.log(“module2”);
src\module3.js
console.log(“module3”);
src\asyncModule1.js
import from ‘lodash’; console.log();

image.png

  • 提取第三方模块,jquery,lodash
  • 提取公共模块

多入口才需要分包,单入口懒加载就够了。不需要走分包逻辑了

6.开启Scope Hoisting 作用域提升,自带的

  • Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 “作用域提升”,是在 Webpack3 中新推出的功能。
  • 初webpack转换后的模块会包裹上一层函数,import会转换成require
  • 代码体积更小,因为函数申明语句会产生大量代码
  • 代码在运行时因为创建的函数作用域更少了,内存开销也随之变小
  • 大量作用域包裹代码会导致体积增大
  • 运行时创建的函数作用域变多,内存开销增大
  • scope hoisting的原理是将所有的模块按照引用顺序放在一个函数作用域里,然后适当地重命名一些变量以防止命名冲突
  • 这个功能在mode为production下默认开启,开发环境要用 webpack.optimize.ModuleConcatenationPlugin插件
  • 也要使用ES6 Module,CJS不支持
    export default 'Hello';
    
    import str from './hello.js';
    console.log(str);
    
    "./src/index.js":
    (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    var hello = ('hello');
    console.log(hello);
    })
    

    函数由两个变成了一个,hello.js 中定义的内容被直接注入到了 main.js 中

7.利用缓存

  • webpack中利用缓存一般有以下几种思路:

    • babel-loader开启缓存
    • 使用cache-loader

      7.1 babel-loader

  • Babel在转义js文件过程中消耗性能较高,将babel-loader执行的结果缓存起来,当重新打包构建时会尝试读取缓存,从而提高打包构建速度、降低消耗

    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: [{
        loader: "babel-loader",
        options: {
          cacheDirectory: true // 启用缓存
        }
      }]
    },
    

    7.2 cache-loader

  • 在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里

  • 存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader

    npm i  cache-loader -D
    
    const loaders = ['babel-loader'];
    module.exports = {
    module: {
      rules: [
        {
          test: /\.js$/,
          use: [
            'cache-loader',
            ...loaders
          ],
          include: path.resolve('src')
        }
      ]
    }
    }
    
  • cache选项是默认开启的,而且是放在内存里的

  • 有了这个cache,其他缓存都不需要了,一个足够。
  • cache-loader是webpack4中配置的,webpack5中不需要配置了

    module.exports = {
      module: {
      mode:'prodution',
      entry:'./src/index.js',
      cache:false,
      .....
    }
    }
    

    8. oneOf

  • 每个文件对于rules中的所有规则都会遍历一遍,如果使用oneOf就可以解决该问题,只要能匹配一个即可退出。(注意:在oneOf中不能两个配置处理同一种类型文件)

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