webpack优化配置

62 阅读25分钟

开发环境性能优化

  • 优化打包构建速度
  • 优化代码调试

生产环境性能优化

  • 优化打包构建速度
  • 优化代码运行的性能

HMR 模块热替换

 HMR :   hot Module replacement 热模块替换 / 模块热替换

**作用: 一个模块发生变化,只会重新打包这一个模块(而不是打包所有的模块)。 极大的提升构建速度

  •  样式文件: 可以使用 HMR 功能,style-loader内部实现
  •  js文件: 默认没有 HMR

    • 需要修改 Js 代码, 添加支持 HMR 功能的 代码
    • 注意:HMR 功能对 JS 的处理, 只能处理 非入口 JS文件的 其他文件
  •  html文件: 默认没有 HMR, 同时会导致问题: html 文件不能热更新了~ (不用做HMR 功能)

    • 解决:修改 Entry 入口, 将 HTML 文件引入。

配置 webpack.config.js

// webpack.config.jsconst { resolve } = require ('path')
​
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    
  // 入口
  // ./src/index.html 加入入口
  entry: {'./src/js/index.js', './src/index.html'}
 
  // 输出
    output: {
        // 输出文件名
        filename: 'bundle.js',
        
        // 输出路径 __dirname node.js变量,当前文件的目录绝对路径
        path: path.resolve(__dirname, 'dist'),
        
        // 自定义资源文件名
        assetModuleFilename: 'images/[hash:10][ext][query]',
    
        // 在生成文件之前清空 output 目录
        clean: true,
  },
    
 // 开发服务器配置
  devServer: {
    // 从目录提供静态文件
    static: {
      directory: resolve(__dirname, "public"),
      publicPath: "/",
    },
​
    // 启动后打开浏览器
    open: true,
​
    // 监听请求的端口号
    port: 8080,
      
    // 开启HMR功能 --> 热模块替换是默认开启的
    // 当修改了 webpack 配置,新配置要想生效,必须重启 Webpac k服务
    hot:true
  },
};

Js 非入口文件 热模块替换

index.js 入口文件
// index.js// 引入
import '../css/iconfont.css';
import '../css/index.css';
improt print from './print'console.log ('index.js 文件被加载了~')
print ();
function add (x,y){
    return x + y;
}
​
console.log (add(1,2));
​
​
if(module.hot) {
    // 一旦 module.hot 为 true, 说明开启了 HMR         功能。 --> 让 HMR 功能代码生效
    
    module.hot.accept('./print.js',function(){
    // 方法会监听 print.js 文件变化, 一旦发生变化      了, 其他默认不会重 新打包构建。
        
        // 会执行后面的回调函数
        
        print ();
    })
}
​
print.js 非入口文件
// print.jsfunction print() {
    // const content = 'hello print';
    
    // 变化
    
    const content = 'hello print ···';
    
    console.log ('阿啊梅')
}
​
export default print

source map 源码映射

source map:   一种提供源代码到构建后代码隐射 技术。 (如果构建后代码出错 了,通过 隐射可以追踪源代码错误。 )

简而言之, SourceMap 就是一个储存着代码位置信息的文件,转换后的代码的每一个位置,所对应的转换前的位置。有了它,点击浏览器的控制台报错信息时,可以直接显示出错源代码位置而不是转换后的代码。

如何使用sourcemap

步骤

  1. 根据源文件,生成source-map文件,webpack在打包时,可以通过配置生成 source-map。
  1. 在转换后的代码,最后添加一个注释,它指向sourcemap; //#sourceMappingURL=common.bundle.js.map。
  1. 浏览器会根据我们的注释,查找响应的source-map,并且根据source-map还原我们的代码,方便进行调试。
  1. 在Chrome中,我们可以按照如下的方式打开source-map:

bc0.png

配置 webpack.config.js

代码实例

  • 如下是一个简单的  index.js 文件,我们故意将最后一行 console.log('hello world') 文件 错写成  console.logo('hello world') 。
const a = 1;
const b = 2;
console.log(a + b);
console.logo('hello world');
无 SourceMap
  • 我们将  webpack.config.js 的  devtool 选项配置为 'none' ,打包上述的 index.js  文件。
// webpack.config.jsmodule.exports = {
    
  // 入口
  // ./src/index.html 加入入口
  entry: {'./src/js/index.js', './src/index.html'}
 
  // 输出
  output: {
    // 输出文件名
    filename: 'bundle.js',
        
    // 输出路径 __dirname node.js变量,当前文件的目录绝对路径
    path: path.resolve(__dirname, 'dist'),
        
    // 自定义资源文件名
    assetModuleFilename: 'images/[hash:10][ext][query]',
    
    // 在生成文件之前清空 output 目录
    clean: true,
 }, 
   
  // 配置 source map
  devtool: "none", 
 
   // 开发服务器配置
  devServer: {
    // 从目录提供静态文件
    static: {
      directory: resolve(__dirname, "public"),
      publicPath: "/",
    },
​
    // 启动后打开浏览器
    open: true,
​
    // 监听请求的端口号
    port: 8080,
      
    // 开启HMR功能 --> 热模块替换是默认开启的
    // 当修改了 webpack 配置,新配置要想生效,必须重启 Webpac k服务
    hot:true
  },
};
  
有 SourceMap
  • 将  webpack.config.js 的  devtool 选项配置由  'none' 改成  source-map 后,再次打包上面的  index.js  文件。
// webpack.config.jsmodule.exports = {
    
  // 入口
  // ./src/index.html 加入入口
  entry: {'./src/js/index.js', './src/index.html'}
 
  // 输出
  output: {
    // 输出文件名
    filename: 'bundle.js',
        
    // 输出路径 __dirname node.js变量,当前文件的目录绝对路径
    path: path.resolve(__dirname, 'dist'),
        
    // 自定义资源文件名
    assetModuleFilename: 'images/[hash:10][ext][query]',
    
    // 在生成文件之前清空 output 目录
    clean: true,
 }, 
   
  // 配置 source map
  devtool: "source-map", 
 
   // 开发服务器配置
  devServer: {
    // 从目录提供静态文件
    static: {
      directory: resolve(__dirname, "public"),
      publicPath: "/",
    },
​
    // 启动后打开浏览器
    open: true,
​
    // 监听请求的端口号
    port: 8080,
      
    // 开启HMR功能 --> 热模块替换是默认开启的
    // 当修改了 webpack 配置,新配置要想生效,必须重启 Webpac k服务
    hot:true
  },
};
  

SourceMap工作原理

  • 我们使用 webpack 打包并选择  devtool 为  source-map 后,每个打包后的 就是 JS 模块会有一个对应的  .map 文件。

    • 打包出来的  main.js.map 文件中,就是一个标准的  SourceMap  内容格式:

      {
        "version": 3,
        "sources": [
          "webpack://webpack5-template/./src/index.js"
        ],
        "names": [],
        "mappings": ";;;;;AAAA;AACA;AACA;AACA;AACA,eAAe;AACf;AACA;AACA,mB",
        "file": "main.bundle.js",
        "sourcesContent": [
          "console.log('Interesting!!!')\n// Create heading node\nconst heading = document.createElement('h1')\nheading.textContent = 'Interesting!'\nconsole.log(a); // 这一行会报错\n// Append heading node to the DOM\nconst app = document.querySelector('#root')\napp.append(heading)"
        ],
        "sourceRoot": ""
      }
      
分析sourcemap

最初  source-map  生成的文件带下是原始文件的  10  倍,第二版减少了约  50% ,第三版又减少了  50% ,所以目前一个  133kb  的文件,最终的  source-map  的大小大概在  300kb。

说明
versionSource Map 的版本,如今最新版本为3
sources源文件列表,转换前的文件。该项是一个数组,表示可能存在多个文件合并。
names转换前的所有变量名和属性名。
mappings压缩混淆后的代码定位源代码的位置信息。 记录位置信息的字符串。这个的话,用到了 VLQ 编码相关。
file该  Source Map 对应文件的名称。转换后的文件名。
sourcesContent源代码字符串列表,用于调试时展示源文件,列表每一项对应于  sources。
sourceRoot源文件根目录,这个值会加在每个源文件之前。转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。
sourceMap格式
// json 代码
{ 
  "version": 3, // source map 的版本。
  "sources": [], // 转换前的文件,该项是一个数组,表示可能存在多个文件合并。
  "names": [], // 转换前的所有变量名和属性名。
  "mappings": "", // 记录位置信息的字符串。
  "file": "", // 转换后的文件名。
  "sourcesContent": [""] // 转换前文件的内容,当没有配置 sources 的时候会使用该项。
}

devtool格式

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

内联 说明 错误提示
inline-source-map 只生成一个内联 source-map , 不是创建一个单独的文件

dist 目录会发现 main.js.map 文件没有了,但在 main.js 文件的最下面,多出了一个很长的 base64 字符串, inline-source-map 会把映射内容以 base64 编码形式加入到打包生成的 JS 文件中去。
inline-source-map source-map 的执行效果是完全一致的只是映射的代码文件一个在 .map文件中一个是内嵌在构建后的 `built.js`中
错误代码的准确信息,源代码的错位位置
eval-source-map 内联 每个文件都生成对应的 source-map,都在 `eval` 里。并且生成一个 DataUrl 形式的 SourceMap

`eval`包裹 js 代码,很容易被 XSS 攻击,存在很大的安全隐患。

`eval` 不可预测,所以将会使用 slow path。
`eval` 我们一般只用于开发环境,不会用于打包线上环境的代码
错误代码的准确信息,源代码的错位位置
外部 说明 错误提示
source-map 打包后的模块在模块后面会对应引用一个 .map 文件,同时在打包好的目录下会针对每一个模块生成相应的 .map 文件,会生成一个 index.js.map 文件,这个文件是一个典型的 sourcemap 文件 错误代码的准确信息,源代码的错位位置
hidden-source-map 仍然会生成 `.map ` 文件,但是 打包后的代码中没有 sourceMappingURL ,也就是说请求代码时浏览器不会加载`.map ` 文件,控制台中看不到源代码
hidden-source-map 执行到错误后隐藏了错误源代码的位置,指挥提示原因和构建后代码的报错位置,相 较于我们的 default状态,他更不会暴露我的源代码。
错误代码的原因,没有源代码错误位置,只有构建后代码位置
nosources-source-map SourceMap 中不包含 sourcesContent 内容,因此 调试时只能看到文件信息和行信息,无法看到源码。 有错误代码准确信息,没有源代码位置
cheap-source-map 生成一个没有列信息 `(column-mappings)` 的 SourceMaps 文件,不包含 loader 的 SourceMaps (譬如 babel 的 sourcemap)
转换代码(行内) 生成的 SourceMaps 没有列映射,从 loaders 生成的 sourcemap 没有被使用
cheap-source-map 和正常的 source-map 相比只能精确到行,而正常的可以精确到列(不包括 的模式), cheap-source-map 在使用 babel-loader 时会自动转译(转译后的源代码会独立格式化分行
有错误代码准确信息,有源代码信息,只能精确到行
cheap-module-source-map 生成一个没有列信息 `(column-mappings)` 的 SourceMaps 文件, 同时 loader 的 sourcemap 也被简化为只包含对应行的。
cheap-module-source-map 也一样不会精确到列,好像是一样的,但是这个不会被 babel-loader 影响,而 cheap-source-map 在使用 babel-loader 时会自动转译(转译后的源代码会独立格式化分行), 因为module模式会把loader的sourcemap也加 进来。
错误代码的准确信息,源代码的错位位置

内联 和 外部 的区别:

  1. 外部生成了文件,内联是没有的。
  1. 内联构建速度更快。
development (开发环境) devtool 配置

开发环境选择就比较容易了,只需要考虑打包速度快、调试方便,官方推荐以下4种:

  • eval
  • eval-source-map
  • eval-cheap-source-map
  • eval-cheap-module-source-map 大多数情况下我们选择 eval-cheap-module-source-map 即可。

速度快(eval>inline>cheap>…

  • eval-cheap-souce-map(首推)
  • eval-source-map

调试更友好:

  • souce-map(首推)
  • cheap-module-souce-map
  • cheap-souce-map

最终平衡速度和调试,开发环境推荐的方案:

  • eval-source-map(调试最友好)
  • eval-cheap-module-souce-map(性能更好)

如果我们使用vue或者react框架开发,都会有对应的脚手架,而脚手架的配置默认是:

  • eval-source-map
production (线上环境) devtool 配置

线上环境官方推荐的 devtool 有4种:

  • none
  • source-map
  • hidden-source-map
  • nosources-source-map 线上环境没有绝对的最优选择一说,根据自己业务需要去选择即可,很多项目也是选择除上述4种之外的 cheap-module-source-map 选项。

内联会让代码体积变大,所以在生产环境不用内联

需要隐藏源代码的方案:

  • nosources-source-map(全部隐藏)
  • hidden-source-map(只隐藏源代码,会提示构建后代码错误信息)

生产环境推荐方案:

  • source-map(调试最友好)
  • cheap-module-souce-map(性能更好)

oneOf

正常来讲,一个文件只能被一个 loader 处理。当一个文件要被多个 loader 处理,那么一定指定 loader 执行的先后顺序:比如 JS 文件先执行 eslint 再执行 babel 。

通过oneOf 规则可以优化构建速度。当每个文件不再需要全部 rules 检查,而只需一条规则解析时,使用 oneOf 确保文件匹配到第一条合适的规则后停止后续匹配。正确安排 oneOf 内 loader 的顺序至关重要,避免同一文件需要多个 loader 处理,例如,将 eslint-loader 置于 oneOf 顶部,以优先处理 JS 文件。

问题:

假如我设置了七八个 loader 处理相应的文件,虽然 test 正则校验文件名称后缀不通过,但是每个文件还是都要经过一下这七八个loader,设置 oneOf 就是处理这个,如果找到了某一个文件的处理 loader,就直接用,不用再过后面的 loader,提高构建速度。

注意:不能有两个配置处理同一种类型文件。比如: eslint-loader 和 babel-loader 都处理同一种文件类型,所以可以把 eslint-loader 提取出来。

配置 Webpack.config.js

// webpack配置
module.exports = {
    // 入口文件
    entry: './src/index.js',
​
    // 输出
  output: {
        // 输出文件名
        filename: 'bundle.js',
        
        // 输出路径 __dirname node.js变量,当前文件的目录绝对路径
        path: path.resolve(__dirname, 'dist'),
        
        // 自定义资源文件名
        assetModuleFilename: 'images/[hash:10][ext][query]',
    
        // 在生成文件之前清空 output 目录
        clean: true,
  },
​
  // 开发服务器配置
  devServer: {
    // 从目录提供静态文件
    static: {
      directory: path.join(__dirname, "public"),
      publicPath: "/",
    },
​
    // 启动后打开浏览器
    open: true,
​
    // 监听请求的端口号
    port: 8080,
  },
​
  // loader配置
  module: {
  
    作用:提升打包构建速度(生产环境)
    rules:[
        {
            test: /.js$/,
            loader: 'eslint-loader',
        },
        {
           
            
            // 以下 loader 只会匹配一个
            // 注意oneOf中不能有两个配置处理同一种类型的文件,
            
            oneOf: [
                // 所以把eslint-loader提出去了
                {
                    test: /.js$/,
                    loader: 'babel-loader'
                },
                {
                    test: /.css$/,
                }
            ]
        }
    ]
  }
 }
 

oneOf的作用就是优化生产环境的打包构建速度。

babel-loader 缓存

针对 js 兼容性进行缓存:(  babel缓存)

 babel-loader 的  options  设置中增加  cacheDirectory  属性,属性值为 true 。表示:开启  babel缓存 ,第二次构建时会读取之前的缓存,构建速度会更快一点。

babel-loader 设置

方式一:在options内添加属性

  • cacheDirectory: true

方式二:

  • use : ['babel-loader?cacheDirectory=true']

配置 Webpack.config.js

// webpack配置
module.exports = {
  	// 入口文件
  	entry: './src/index.js',

  	// 输出
  output: {
    	// 输出文件名
    	filename: 'bundle.js',
        
    	// 输出路径 __dirname node.js变量,当前文件的目录绝对路径
    	path: path.resolve(__dirname, 'dist'),
        
    	// 自定义资源文件名
    	assetModuleFilename: 'images/[hash:10][ext][query]',
    
    	// 在生成文件之前清空 output 目录
    	clean: true,
  },

  // 开发服务器配置
  devServer: {
    // 从目录提供静态文件
    static: {
      directory: path.join(__dirname, "public"),
      publicPath: "/",
    },

    // 启动后打开浏览器
    open: true,

    // 监听请求的端口号
    port: 8080,
  },

  // loader配置
  module: {
  
 	作用:提升打包构建速度(生产环境)
	rules:[
        {
            test: /.js$/,
            loader: 'eslint-loader',
        },
        {
           
            
            // 以下 loader 只会匹配一个
            // 注意oneOf中不能有两个配置处理同一种类型的文件,
            
            oneOf: [
              {
                test: /.css$/,
                use: [...commonCssLoader]
              },
                  
              {
                test: /.less$/,
                use: [...commonCssLoader, 'less-loader']
              },
                  
              /*
                正常来讲,一个文件只能被一个loader处理。
                当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:
                  先执行eslint 在执行babel
              */
			 {
        		test: /.js$/,
        		exclude: /node_modules/,
        		use: {
          			loader: "babel-loader",
          			options: {
                    // 预设,提示babel怎么样做兼容性处理
                    // 基本的兼容性处理,
                    presets: [
                              [
                                "@babel/preset-env",
                                {
                                  // 按需加载
                                  useBuiltIns: "usage",
                                  // 指定core-js版本
                                  corejs: {
                                    version: 3,
                                  },
                                  // 指定兼容性到哪个版本浏览器
                                  targets: {
                                    chrome: "60",
                                    firefox: "60",
                                    ie: "9",
                                    safari: "10",
                                    edge: "17",
                                  },
                                },
                              ],
                            ],
                            // 开启babel缓存
              				// 第二次构建时,会读取之前的缓存
                            cacheDirectory: true cacheDirectory:true,  
                            
                            // 关闭缓存文件压缩
        					cacheCompression:false, 
       						
                            // 减少代码体积
                            plugins:["@babel/plugin-transform-runtime"],   

          				},
            		},
      			},
            ]
        }
	]
  }
 }

文件资源缓存

注意,上面的服务器设置了 maxAge ,即开启 vv 。 通过  network  查看资源,刷新页面,可以看到资源来自  cache,即来自缓存,查看请求可以发现请求头设置了  Cache-Control,  max-age=3600,即有效期为  3600s,一个小时,意思就是这个资源会被强制缓存一个小时

bc1.png

这个缓存会带来新的问题:

假如我们修改代码,修改一下js代码

import '../css/index.css';

function sum(...args) {
  return args.reduce((p, c) => p + c, 0);
}

// eslint-disable-next-line
// console.log(sum(1, 2, 3, 4));// 修改前

// eslint-disable-next-line
console.log(sum(1, 2, 3, 4, 5));// 修改后

再次构建,打开浏览器刷新,会发现结果没有变化

bc2.png

这是因为当前资源在强制缓存期间,它是不会访问服务器的,直接读取本地缓存  这就带来了一个问题,假使我们的资源在强缓存期间出现了严重的 bug,开发人员需要紧急修复,但因为资源被强制缓存,就算修复了,也会因为还在强缓存期间而无效。

我们可以通过修改资源名称来解决这个问题

文件资源缓存

在打包输出文件的文件名中添加hash值

 hash: 根据每次打包后 wepack 生成的 hash 值不同来加载资源会因为使用的都是 wepack 每 次打包后的 hash 值,导致改动一处,其他资源都改动,都重新加载了。 问题: 因为 jscss 同时使用一个 hash

如果重新打包,会导致所有缓存失效。(虽然只改动一个文件)

 chunkhash: 打包来源同一个 chunk(代码块) 生成的 hash 值就一样 如果 cssjs 中引入,则因为属于同一个 chunk (即是否属于同一个 entry ),导致改动一处,其 他资源都改动,都重新加载了。 ​ 问题: jscsshash 值还是一样的

因为css是在js中被引入的,所以同属于一个chunk

 contenthash:  根据文件的内容生成 hash 值。不同文件 hash 值一定不一样 。必须使 用了插件: extract-text-webpack-plugin/mini-css-extract-plugin

--> 让代码上线运行缓存更好使用

静态资源服务器

server.js

// server.js// npm i express nodemon -S
// node server.js 或者 nodemon server.js
/*
  服务器代码
  启动服务器指令:
    npm i nodemon -g
    nodemon server.js 
    
    不需要下载nodemon
    node server.js
  访问服务器地址:
    http://localhost:3000
​
*/
​
const express = require('express');
​
const app = express();
// express.static向外暴露静态资源
// maxAge 资源缓存的最大时间,单位ms
app.use(express.static('build', { maxAge: 1000 * 3600 }));
​
app.listen(3000);
​
​
配置 webpack.config.js
// webpack.config.jsconst { resolve } = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
​
/*
  缓存:
    babel缓存
      cacheDirectory: true
      --> 让第二次打包构建速度更快
    文件资源缓存
      hash: 每次wepack构建时会生成一个唯一的hash值。
        问题: 因为js和css同时使用一个hash值。
          如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
      chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
        问题: js和css的hash值还是一样的
          因为css是在js中被引入的,所以同属于一个chunk
      contenthash: 根据文件的内容生成hash值。不同文件hash值一定不一样    
      --> 让代码上线运行缓存更好使用
*/// 定义nodejs环境变量:决定使用browserslist的哪个环境
process.env.NODE_ENV = 'production';
​
// 复用loader
const commonCssLoader = [
  MiniCssExtractPlugin.loader,
  'css-loader',
  {
    // 还需要在package.json中定义browserslist
    loader: 'postcss-loader',
    options: {
      ident: 'postcss',
      plugins: () => [require('postcss-preset-env')()]
    }
  }
];
​
module.exports = {
    // 入口文件
    entry: './src/index.js',
​
    // 输出
  output: {
      
        // contenthash 只有在内容发生改变才会变
        filename: '[name].[contenthash].js', 
      
        //输出路径   __dirname 代表当前文件的绝对路径
        path: path.resolve(__dirname, 'dist'), 
        
        // 自定义资源文件名
        //设置图片输出路径以及文件名称
        assetModuleFilename: 'images/[hash:10][ext][query]',
    
        // 在生成文件之前清空 output 目录
        clean: true,
  },
​
  // 开发服务器配置
  devServer: {
    // 从目录提供静态文件
    static: {
      directory: path.join(__dirname, "public"),
      publicPath: "/",
    },
​
    // 启动后打开浏览器
    open: true,
​
    // 监听请求的端口号
    port: 8080,
  },
​
  // loader配置
  module: {
    rules: [
      {
        // 在package.json中eslintConfig --> airbnb
        test: /.js$/,
        exclude: /node_modules/,
        // 优先执行
        enforce: 'pre',
        loader: 'eslint-loader',
        options: {
          fix: true
        }
      },
      {
        // 以下loader只会匹配一个
        // 注意:不能有两个配置处理同一种类型文件
        oneOf: [
          {
            test: /.css$/,
            use: [...commonCssLoader]
          },
          {
            test: /.less$/,
            use: [...commonCssLoader, 'less-loader']
          },
          /*
            正常来讲,一个文件只能被一个loader处理。
            当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:
              先执行eslint 在执行babel
          */
          {
            test: /.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'usage',
                    corejs: { version: 3 },
                    targets: {
                      chrome: '60',
                      firefox: '50'
                    }
                  }
                ]
              ],
​
              // 开启babel缓存
              // 第二次构建时,会读取之前的缓存
              cacheDirectory: true cacheDirectory:true,  
                            
             // 关闭缓存文件压缩
             cacheCompression:false, 
                            
             // 减少代码体积
             plugins:["@babel/plugin-transform-runtime"], 
          },
            
          // 图片资源
          {
            test: /.(jpg|png|gif)/,
            loader: 'url-loader',
            options: {
              limit: 8 * 1024,
              name: '[hash:10].[ext]',
              outputPath: 'imgs',
              esModule: false
            }
          },
          
         // html 资源
          {
            test: /.html$/,
            loader: 'html-loader'
          },
           
          // 其他资源
          {
            exclude: /.(js|css|less|html|jpg|png|gif)/,
            loader: 'file-loader',
            options: {
              outputPath: 'media'
            }
          }
        ]
      }
    ]
  },
    
  // 插件配置
  plugins: [
    //默认传键html文件,并引入打包输出的资源,默认为基本结构
    new MiniCssExtractPlugin({
      filename: 'css/built.[contenthash:10].css'
    }),
    new OptimizeCssAssetsWebpackPlugin(),
      
    //将模板复制成指定文件
    new HtmlWebpackPlugin({
      template: './src/index.html',
     minify: {
        collapseWhitespace: true, //折叠空格
        removeComments:true  //移出注释
      }
    })
  ],
    
  // 模式
  mode: 'production',
    
   
  devtool: 'source-map'
};
​
​

tree shaking 树摇

Tree shaking : 是一个用于优化 JavaScript  应用程序的术语,特别是在构建过程中减少生成的代码大小。它的主要目标是消除未使用的代码,即那些在应用程序中没有被引用或调用的部分,从而减小最终生成的 JavaScript 文件的大小。

Webpack 中,启动 Tree shaking功能必须同时满足三个条件:

  • 使用 ESM 规范编写模块代码 (必须使用ES6模块化)
  • 配置 optimization.usedExports 为 true,启动标记功能
  • 启动代码优化功能,可以通过如下方式实现:
    • 配置 mode = production
    • 配置 optimization.minimize = true
    • 提供 optimization.minimizer  数组

JS-Tree-Shaking

开发环境
配置 webpack.config.js

webpack.config.js 配置, 告诉 webpack 只打包导入模块中用到的内容:

// webpack.config.jsmodule.exports = {
  // mode: "production",
  mode: "development",
  devtool: false,
  optimization: {
    // 目的使未被使用的export被标记出来
    usedExports: true, 
  },
};
​
​
配置 package.json

package.json 配置, 告诉 webpack 哪些文件不做 Tree shaking:

"sideEffects": false --> 所有的代码都没有副作用。 (都可以进行 tree- shaking)

 问题:

可能会把 Css/ @babel/polyfill (副作用) 文件干掉

 "sideEffects": ["*.css", "*.less", "*.scss"],
生产环境

无需进行任何配置, webpack 默认已经实现了Tree shaking

注意点: 只有 ES Modle 导入才支持 Tree-Shaking 任何导入的文件都会受到 Tree-Shaking 的影响。

CSS-Tree-Shaking

github.com/webpack-con…

安装相关插件
npm i -D purifycss-webpack purify-css glob-all
配置webpack.config.js
// webpack.config.js

const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')

const PATHS = {
  src: path.join(__dirname, 'src')
}

modeule.exports = {
    plugins: [
        new MiniCssExtractPlugin({
            filename: "[name].css",
        }),
        new PurgeCSSPlugin({
            paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true }),
        }),
    ]
}
作用

Tree shaking 的主要作用是优化前端应用程序的性能,特别是在减小生成的 JavaScript 文件大小方面发挥关键作用。

作用描述
减小文件大小Tree shaking 通过静态分析代码,识别和移除未使用的代码块。这样可以消除应用程序中未被引用或调用的部分,从而减小最终生成的 JavaScript 文件的大小。
提高加载速度通过减小文件大小,Tree shaking 有助于提高应用程序的加载速度。用户在访问网站时需要下载的 JavaScript 代码更少,因此页面加载时间更短,用户体验更好。
网络传输优化较小的文件大小意味着在网络上传输数据的成本更低。这对于用户在慢速或不稳定的网络条件下访问网站时尤为重要,可以减少加载时间和提高可访问性。
资源利用率通过消除未使用的代码,Tree shaking 可以提高资源的利用率。只有实际需要的代码被包含在最终的构建中,因此减小了浏览器需要处理的代码量。
版本控制和部署Tree shaking 在版本控制和部署过程中也有帮助。较小的文件大小意味着在版本控制系统中占用的空间更小,并且在部署到生产环境时传输的数据更少,减少了部署时间和成本。
优化用户体验更快的加载速度和更小的文件大小可以显著改善用户体验。用户更倾向于与快速加载的应用程序进行交互,因此通过 Tree shaking 优化可以提高应用的用户满意度。

code split 代码分割

打包代码时会将所有 js 文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。

所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。

Code Split是什么

代码分割(Code Split)主要做了两件事:

  1. 分割文件:将打包生成的文件进行分割,生成多个 js 文件。
  2. 按需加载:需要哪个文件就加载哪个文件。

通过入口文件进行代码分割

多入口
文件目录
├── public
├── src
|   ├── app.js
|   └── main.js
├── package.json
└── webpack.config.js 
安装依赖
npm i webpack webpack-cli html-webpack-plugin -D
新建文件

内容无关紧要,主要观察打包输出的结果

app.js
console.log("hello app");
main.js
console.log("hello main");
配置 webpack.config.js
// webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // 单入口
  // entry: './src/main.js',
 
  // 多入口: 有一个入口,最终输出就有一个 bundle
  entry: {
    main: "./src/main.js",
    app: "./src/app.js",
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    // [name]是webpack命名规则,使用chunk的name作为输出的文件名。
      
    // 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
      
    // chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意		是前面的xxx,和文件名无关。
      
    // 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做		main.js会发生覆盖。(实际上会直接报错的)
      
    filename: "js/[name].js",
    clear: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
  mode: "production",
};
运行指令
npx webpack

此时在 dist 目录我们能看到输出了两个 js 文件。

总结: 配置了几个入口,至少输出几个 js 文件。

多入口提取公共模块

如果多入口文件中都引用了同一份代码,我们不希望这份代码被打包到两个文件中,导致代码重复,体积更大。我们需要提取多入口的重复代码,只打包生成一个 js 文件,其他文件引用它就好。

实例:

修改文件

app.js

//	app.js

import { sum } from "./math";

console.log("hello app");
console.log(sum(1, 2, 3, 4));

main.js

// main.js

import { sum } from "./math";

console.log("hello main");
console.log(sum(1, 2, 3, 4, 5));

math.js

// math.js

export const sum = (...args) => {
  return args.reduce((p, c) => p + c, 0);
}
配置 webpack.config.js
// webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // 多入口
  entry: {
    main: "./src/main.js",
    app: "./src/app.js",
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "js/[name].js",
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
  mode: "production",
  
  // 压缩代码
  optimization: {
    
    // 代码分割配置
    splitChunks: {
        
    // 动态导入的模块其依赖会根据规则分离    
    chunks: 'async', 
        minSize: 30000, 
    
        // 文件至少被 1 个chunk 引用 
        minChunks: 1, 
    
        // 动态导入文件最大并发请求数为 5
        maxAsyncRequests: 5, 
    
        // 入口文件最大并发请求数为 3
        maxInitialRequests: 3, 
    
        // 文件名中的分隔符
        automaticNameDelimiter: '~', 
    
        // 自动命名
        name: true,
    
        
        cacheGroups: {
      
      // 分离第三方库      
      vendors: { 
        test: /[\/]node_modules[\/]/,
        
        // 权重  
        priority: -10 
      },
      
     // 分离公共的文件       
     default: { 
        
        // 文件至少被 2 个 chunk 引用
        minChunks: 2, 
        priority: -20,
        
        // 复用存在的 chunk  
        reuseExistingChunk: true 
      }
    }
  }

chunks  

该参数有四种取值

  • async   动态导入的文件其静态依赖会根据规则分离
  • initial  入口文件的静态依赖会根据规则分离
  • all   所有的都会根据规则分离
  • chunks  ==> Boolean   返回 true 表示根据规则分离,false 则不分离
运行指令
 npx webpack

此时我们会发现生成 3 个 js 文件,其中有一个就是提取的公共模块。

单入口

开发时我们可能是单页面应用(SPA),只有一个入口(单入口)。那么我们需要这样配置:

配置 webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
 
module.exports = {
  // 单入口
  entry: "./src/main.js",
  // 多入口
  // entry: {
  //   main: "./src/main.js",
  //   app: "./src/app.js",
  // },
  output: {
    path: path.resolve(__dirname, "./dist"),
    // [name]是webpack命名规则,使用chunk的name作为输出的文件名。
      
    // 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
      
    // chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意       是前面的xxx,和文件名无关。
      
    // 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做      main.js会发生覆盖。(实际上会直接报错的)
      
    filename: "js/[name].js",
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
  mode: "production",
  /*
    可以将 node_module 中代码单独打包 一个 chunk 最终输出
  */
  optimization: {
    // 代码分割配置
    splitChunks: {
      // 对所有模块都进行分割
      chunks: "all", 
      /* 
      
      以下是默认值:
      
      // 分割代码最小的大小
      minSize: 20000, 
      
      // 类似于minSize,最后确保提取的文件大小不能为0
      minRemainingSize: 0, 
      
      // 至少被引用的次数,满足条件才会代码分割
      minChunks: 1, 
      
      // 按需加载时并行加载的文件的最大数量
      maxAsyncRequests: 30, 
      
      // 入口js文件最大并行请求数量
      maxInitialRequests: 30, 
      
      // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、      maxInitialRequests)
      enforceSizeThreshold: 50000, 
      
      // 组,哪些模块要打包到一个组
      cacheGroups: {
      
      // 组名
      defaultVendors: {
      
      // 需要打包到一起的模块
      test: /[\/]node_modules[\/]/, 
      
      // 权重(越大越高)
      priority: -10, 
      
      // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新         的模块
      reuseExistingChunk: true, 
         },
          
          // 其他没有写的配置会使用上面的默认值
          default: { 
          
          // 这里的minChunks权重更大
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
       },
      },
     */
  },
};

按需加载,动态导入

当涉及到动态代码拆分时,webpack 提供了两个类似的技术。

第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。

第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure 。

使用 import() 来进行动态导入

  • 语法:
import(/* webpackChunkName: chunkName */ chunk)
.then( res => {
    // handle something
})
.catch(err => {
    // handle err 
    // 加载失败
});

/* chunkName */ 为指定代码分割包名,chunk指定需要代码分割的文件入口。注意不要把 import关键字和import()方法弄混了,该方法是为了进行动态加载。

main.js

console.log("hello main");

document.getElementById("btn").onclick = function () {
  // 动态导入 --> 实现按需加载
  // 即使只被引用了一次,也会代码分割
  import("./math.js").then(({ sum }) => {
    alert(sum(1, 2, 3, 4, 5));
  });
};

app.js

console.log("hello app");

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-				scale=1.0"/>
    <title>Code Split</title>
  </head>
  <body>
    <h1>hello webpack</h1>
    <button id="btn">计算</button>
  </body>
</html>
配置 webpack.config.js
// webpack.config.js:

var webpack = require('webpack');
var path = require('path');
module.exports = {
    entry: {
        'pageA': './src/pageA',
        
        // 指定单独打包的第三方库(和CommonsChunkPlugin结合使用),可以用数组指定多个
        'vendor': ['lodash'] 
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].bundle.js',
        
        // code splitting的chunk是异步(动态)加载,需要指定chunkFilename(具体可以			  了解和filename的区别)
        // 给打包输出的其他输出文件命名
        chunkFilename: '[name].chunk.js', 
        
        // 动态加载的路径
        publicPath: './dist/' 
    },
    plugins: [
        // 为第三方库和和manifest(webpack runtime)单独打包   
    	new webpack.optimize.CommonsChunkPlugin({
        
        	// 为异步公共加载的代码打一个的包
        	async: 'async-common', // 异步公共的代码
            
            // 要加上children,会从入口的子依赖开始找
        	children: true, 
        	name: ['vendor', 'manifest'],
        	minChunks: Infinity
        }),
    ]
}

require.ensure代码分割

  • 语法:

    require.ensure(dependencies: String[], callback: function(require), chunkName: String)
    ​
    
  • 参数:

    • 第一个参数是 dependencies  依赖列表,webpack会加载模块,但不会执行。
    • 第二个参数是一个回调,当所有的依赖都加载完成后,webpack 会执行这个回调函数,在其中可以使用 require  导入模块,导入的模块会被代码分割到一个分开的 chunk 中。
    • 第三个参数指定第二个参数中代码分割的 chunkname。

    src/pageA.js

    // src/pageA.jsimport * as _ from 'lodash';
    import subPageA from './subPageA';
    import subPageB from './subPageB';
    console.log('this is pageA');
    export default 'pageA';
    ​
    

    src/subPageA.js

     // src/subPageA.jsimport module from './module';
    console.log('this is subPageA');
    export default 'subPageA';
    ​
    

    src/subPageB.js

     // src/subPageB.js
    ​
      import module from './module';
      console.log('this is subPageB');
      export default 'subPageB';
    

    src/module.js

     // src/module.jsconst s = 'this is module'
    export default s;
    ​
    

 修改src/pageA.js,把import导入方式改成 require.ensure的方式就可以代码分割

​
       // import subPageA from './subPageA';
       // import subPageB from './subPageB';
​
       require.ensure([], function() {
        // 分割./subPageA模块
          var subPageA = require('./subPageA');
       }, 'subPageA');
​
       require.ensure([], function () {
            var subPageB = require('./subPageB');
        }, 'subPageB'); 
    

用了 require.ensure 的模块被代码分割了,达到了我们想要的目的。由于 subPageA  和 subPageB  有公共模块 module.js ,打开 subPageA.chunk.js  和 subPageB.chunk.js 发现都有公共模块module.js ,这时候就需要在 require.ensure  代码前面加上require.include('./module') 

require.include('./module'); // 加在require.ensure代码前
配置 webpack.config.js
output: {
    ...
    publicPath: './dist/' // 动态加载的路径
}

统一资源输出

配置 webpack.config.js
module.exports = {
    //  入口
    entry: "./src/main.js"
    
    // 输出
    output: {
        //  所有文件的输出路径
        //  __dirname node.js 的变量, 代表当前文件的文件夹目录
        
        path: path.resolve (__dirname, "../dist"), // 绝对路径
        
        //  入口文件打包输出文件名
        filename: "static/js/[name].js",
        
        // 给打包输出的其他文件命名
        chunkFilename: "static/js/[name].chunk.js",
        
        // 图片、字体等 通过 type: "asset" 处理的资源命名方式
        assetModuleFilename: "static/media/[hash:10][ext][query]",
        
        // 自动清空上次打包的内容
        // 原理: 在打包前,将 path 整个目录内容清空,再进行打包
        clean: true,
    },
    
    // 加载器
    module: {
    
        rules: [
            oneOf: [
                {
                    test:/.css$/,  // 只检测 .css 文件
                    use: getStyleLoader()   // 执行顺序,从右到左 (从下到上)
                },
                
                {
                    test:/.less$/, 
                    
                    // loader: 'xxx' 只能使用1个loader
                    use: getStyleLoader(less-loader)  
                },
                
                {
                    test:/.s[ac]ss$/,  
                    use: getStyleLoader(sass-loader)  
                },
                
                {
                    test:/.styl$/, 
                    use: getStyleLoader(stylus-loader)  
                },
                
                {
                    test:/.(png|jpe?g|gif|webp|svg)$/, 
                    type: "asset",
                    parser: {
                        dataUrlCondition: {
                            // 小于 10 kb的图片转 base64
                            // 优点: 减少请求数量, 缺点: 体积会更大
                            
                            maxSize: 10 * 1024, // 10kb
                        }
                    },
                    
                    generator: {
                    
                        // 输出图片名称
                        //  [hash: 10] 取 hash值 前10位
                        
                        filename: 'static/images/[hash:10][ext][query]'
                    }
                },
                
                {
                    test: /.js$/,
                    
                    // exclude: /node_modules/, 排除 node_modules 下的文件,
                                其他文件都处理
                                
                    // 只处理 Src 下的文件, 其他文件不处理
                    include: path.resolve (__dirname,"../src") 
                    
                    use: [
                        {
                            loader: "thread-loader", // 开启多进程
                            option: {
                                works: threads,  // 进程数量
                            }
                        },
                        
                        {
                            loader: "babel-loader",
                            option: {
                                // presets : ["@babel/preser-env"]
                                cacheDirectory: true,  // 开启babel 缓存
                                cacheCompression: false,  // 关闭缓存文件压缩
                                
                                // 减少代码体积
                                plugin: [ 
                                    "@babel/plugin-transform-runtime" 
                                ],  
                            }
                        }
                    
                    ]
                    
                }
            ]
        ]
    },
    
    // 插件 
    plugins: [
    
        // plugin 的配置
        new ESLintPlugin ({
            // 检测哪些文件
            conntext: path.resolve(__dirname,"../src"),
            exclude: "node_modules", // 默认值
            cache: true,  // 开启缓存
            cacheLocation: path.resolve(
                __dirname,"../node_modules/.cache/eslintcache"
            ),
            
            threads, // 开启多进程和设置进程数量
        }),
        
        new HtmlWebpackPlugin ({
            // 模版, 以 public/index.html 文件创建 新的 Html 文件
            // 新的 Html 文件 特点: 1. 结构和原来一致  2. 自动引入打包输出的资源
            template: path.resolve(__dirname,"../public/index.html")
        }),
        
        new MinCssExtractPlugin({
            // 多入口 css
            filename: "static/css/[name].css",
            
            // 给打包输出的其他css文件命名
            chunkFilename: "static/css/[name].chunk.css",
        })
        
        
    ]
}

懒加载和预加载

我们前面已经做代码分割,同时会使用  import 动态导入语法来进行代码 按需加载 (我们也叫懒加载, 比如路由懒加载就是这样实现的)。

但是加载速度还不够好, 比如 : 用户点击按钮时 才加载这个资源的, 如果资源体积很大, 那么用户就会感觉到明显的卡顿效果。

我们想在浏览器空闲时间,加载后续需要使用的资源。 我们就需要 用上  Preload 和  Prefetch 技术。

懒加载

懒加载或者按需加载,会在文件需要使用时才加载,是一种很好的优化网页或应用的方式。

懒加载的使用加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。

index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>webpack</title>
</head>

<body>
  <h1>hello lazy loading</h1>
  <button id="btn">按钮</button>
</body>

</html>
test.js
console.log('test.js文件被加载了~');
​
export function mul(x, y) {
  return x * y;
}
​
export function count(x, y) {
  return x - y;
}
​
index.js
// index.jsconsole.log('index.js文件被加载了~');
​
import { mul } from './test';
​
document.getElementById('btn').onclick = function() {
    console.log(mul(4, 5));
};
​
懒加载写法
index.js
// index.jsconsole.log('index.js文件被加载了~');
​
// import { mul } from './test';document.getElementById('btn').onclick = function() {
  // 懒加载~:当文件需要使用时才加载~
  import(/* webpackChunkName: 'test'*/'./test').then(({ mul }) => {
    console.log(mul(4, 5));
  });
};
​

懒加载其实就是用到了  code splitting 文章中动态导入的方法

是什么

Preload(预加载): 告诉浏览器立即加载资源 

<!-- 使用 link 标签静态标记需要预加载的资源 -->
<link rel="preload" href="/path/to/style.css" as="style"><!-- 或使用脚本动态创建一个 link 标签后插入到 head 头部 -->
<script>
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/path/to/style.css';
document.head.appendChild(link);
</script>

href   表示需要预加载的资源路径 

as属性  指定预加载资源的类型有“script’ /‘style’/ "font"

相比之下, Prefetch   是在页面加载后不紧急需要但将来可能需要使用的资源进行预加载。

Prefetch (预获取):  告诉浏览器空闲时才加载资源 

<link rel="prefetch" href="path/to/resource"/>

prefetch(预获取) / preload(预加载) 两种方式

函数的方式
math.js 文件

首先创建 math.js 文件。并且添加两个函数

// math.js export const add = (x, y) => {
  return x + y;
};
​
export const sub = (x, y) => {
  return x - y;
};
index.js

在 index.js 入口文件中使用 math.js

// index.jsconst btn = document.createElement("button");
​
btn.textContent = "按钮";
​
btn.addEventListener("click", () => {
  // /* webpackChunkName: 'math' */ 指定打包后文件名为 math
  //  最终文件名为 math.build.js ,这是因为 在 webpack.config.js 配置 output 下      的 filename 
    
  import(/* webpackChunkName: 'math' */ "./math.js").then(({ add }) => {
    console.log(add(1, 2))
  });
});
​
document.body.appendChild(btn);
执行 npx webpack

执行 npx webpack 打包 src 目录下文件。结果如下:生成了 新的 math.build.js

bc3.png

执行 npx webpack-dev-server 启动 web  服务器。页面展示一个按钮,并且点击按钮后,才会加载 math.build.js 文件。

bc4.png

prefetch 预加载

index.js

// index.jsconst btn = document.createElement("button");
​
btn.textContent = "按钮";
​
btn.addEventListener("click", () => {
  // webpackPrefetch 设置 为 true
    import(/* webpackChunkName: 'math',webpackPrefetch: true */             "./math.js").then(({ add }) => {
    
        console.log(add(1, 2))
     
  });
});
​
document.body.appendChild(btn);

启动 webpack 服务器。

npx webpack-dev-server

默认地址是: 127.0.0.1:8080。 刷新浏览器,查看 network,可以发现math.build.js 已经加载下来。 并且 index.html 文件,增加了link 标签,链接到 math.build.js 文件。 rel=“prefetch” 这里使用 prefetch 预加载,当我们首页内容加载完毕,在网络空闲的时候,才会加载 link对应的文件

index.html

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="prefetch" as="script" href="http://127.0.0.1:8080/math.build.js">
</head>
preload 预加载

打开  index.js 文件, 在  import() 函数中,加入参数 webpackPreload: true

index.js

// index.jsconst btn = document.createElement("button");
​
btn.textContent = "按钮";
​
btn.addEventListener("click", () => {
  // webpackPreload: true
  import(/* webpackChunkName: 'math', webpackPreload: true */ "./math.js").then(({ add }) => {
    console.log(add(1, 2))
  });
});
​
document.body.appendChild(btn);

效果和懒加载一样,也是点击按钮后才会加载  math.js 文件。

配置 webpack.config.js
// webpack.config.js let path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin")
const MinCssExtractPlugin = require("mini-css-extract-plugin")
const CssMinmizerPlugin = require("css-minimizer-webpack-plugin")
​
module.exports = {
  // 入口文件
  entry: {
    // index: {
    //   import: "./src/index.js",
    //   dependOn: 'shared'
    // },
​
    // another: {
    //   import: "./src/index2.js",
    //   dependOn: 'shared'
    // },
    
    // // 当两个入口文件有 lodash 模块时就会抽离出来,并且取名 为 shared.js
    // shared: 'lodash'
    
    index: "./src/index.js",
    another: "./src/index2.js"
​
  },
    
  // 打包后文件
  output: {
    // 配置打包后,入口文件名字
    filename: "[name].build.js",
    path: path.resolve(__dirname, './dist'),
    clean: true,  // 每次打包前先删除 dist 目录
​
    // contenthash 表示哈希值
    // ext 表示扩展名
    // 图片资源文件 打包后输出
    assetModuleFilename: "images/[contenthash][ext]"
​
  },
​
  // 模式 development/production
  mode: 'development',
    
  // 打包后,可以方便的调试代码。代码的位置和源文件一致。
  devtool: "inline-source-map",
​
  // 插件使用。
  // HtmlWebpackPlugin 用于自动生成 dist 目录下 index.html 魔板文件
  plugins: [
    new HtmlWebpackPlugin({
      // 根据魔板文件生成
      template: "./index.html",
      // 生成 dist 目录中 html 的文件名
      filename: "app.html",
      // 生成的 js 文件 引入到body标签中
      inject: "body",
      
      minify: {
         collapseWhitespace: true, //折叠空格
         removeComments:true  //移出注释
       }
    }),
​
    // 把 css 合并成一个文件
    new MinCssExtractPlugin({
      // 打包文件,放到 dist 下的 styles 目录下
      // contenthash 哈希字符串
      filename: "styles/[contenthash].css"
    })
​
  ],
​
  // 指定 dist 作为根路径
  devServer: {
    static: "./dist"
  },
​
  // 配置打包 其它资源文件 规则
  module: {
    rules: [
      {
        test: /.png$/,
        type: "asset/resource",
​
        // 打包后文件命名 
        // 优先级高于 output 下 assetModuleFilename
        generator: {
          filename: "images/[contenthash][ext]"
        }
      },
      {
        test: /.svg$/,
        // 导出资源类型的 dataurl (base64格式)
        type: "asset/inline"
      },
      {
        test: /.txt$/,
        // 导出资源的源代码
        type: "asset/source"
      },
      {
        test: /.jpg$/,
        // 会根据 resource / inline 两种方式进行选择
        // 默认情况下,当资源文件大于 8k 选择 resource 模式生成资源
        // 当资源文件小于8k 选择 inline 生成 base64 数据。
        type: "asset",
        parser: {
          dataUrlCondition: {
            // 设置临界值,超过 maxSize 就会 使用  asset/resource 模式,否则使用                 asset/inline 生成 base64 代码
            maxSize: 4 * 1024 * 1024   // 4 M
          }
        }
      },
      {
        test: /.(css|less)$/,
        
        // loader 执行顺序是从右到左(三个loader位置不能颠倒)
        use: [MinCssExtractPlugin.loader, 'css-loader', 'less-loader']
      },
      {
        test: /.(woff|woff2|eot|ttf|otf)$/,
        // asset/resource 可以帮助我们载入任何类型的资源
        type: "asset/resource"
      },
      {
        test: /.(csv|tsv)$/,
        use: "csv-loader"
      },
      {
        test: /.xml$/,
        use: "xml-loader"
      },
      {
        test: /.js$/,
        // 排除 node_modules 目录下的 js 文件
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"]
          }
        }
      }
    ]
​
  },
​
  // 优化配置
  optimization: {
    minimizer: [
      new CssMinmizerPlugin()
    ],
​
    // 实现代码分割,并且保存到单独的一个文件里。
    splitChunks: {
      chunks: 'all'
    }
​
  }
}
插件方式(plugin)
安装依赖
$ npm install --save-dev preload-webpack-plugin
配置 webpack.config.js
// webpack.config.js

const PreloadWebpackPlugin = require('preload-webpack-plugin');
 
module.exports = {
  entry: {

    app: './src/main.js',

    preload: './src/test.js'  //定义需要预加载的文件

  },
  plugins: [
    // ... 其他插件
    new PreloadWebpackPlugin({
      rel: 'preload', // preload兼容性更好
        
    	as(entry) {  //资源类型
          if (/.css$/.test(entry)) return 'style';
          if (/.woff$/.test(entry)) return 'font';
          if (/.png$/.test(entry)) return 'image';
          return 'script';
    	},
 
      // 可以是 'initial', 'async', 'all', 或者指定 chunk 名称的数组
      include: ['preload'] 
      
      // rel: 'prefetch', // prefetch兼容性更差
 	  // include: 'allChunks',
  	  fileWhitelist: [/.css$/, /.js$/], // 资源白名单
      fileBlacklist: [/.svg/] // 资源黑名单
                      
  	  // 可以是 'low', 'medium', 'high' 或者一个正整数值 
      priority: 'low' ,                                    
	})
	//我们将优先级设置为 "low",这意味着被预加载的资源将被赋予较低的优先级。你也可以将优		先级设置为 "medium" 或 "high",以指定更高的优先级

                         
    }),
  ],
  // ... 其他webpack配置
};

Preload和Prefetch的共同点

  • 都只会加载资源,并不执行。
  • 都有缓存

Preload和Prefetch的区别

  • 一个 预加载块(preload) 开始与父块并行加载。预取的块(prefetch) 在父块完成加载后启动。

  • 预加载块(preload) 具有中等优先级,可以立即下载。而 预取块(prefetch) 在浏览器空闲时下载预取的块。

  • 一个 预加载块(preload) 应该被父块立即请求。预取块(prefetch) 可以在将来的任何时候使用。

  • 浏览器的支持能力是不同的。

    虽然说prefetch 或者是 preload 好,但是我们千万要记得,不要所有的异步加载模块都使用这个东西,我们应该根据自己的业务去加载,否则页面性能不是达到最优而是长时间的等待加载。

加载方式的对比

  • 正常加载:可以认为是并行加载(同一时间加载多个文件)
  • 懒加载:当文件需要使用时才加载~
  • 预加载 prefetch:会在使用之前,提前加载js文件 等其他资源加载完毕,浏览器空闲了,再偷偷加载资源

最佳实践

基于上面对使用场景的分享,我们可以总结出一个比较通用的最佳实践:

  • 大部分场景下无需特意使用preload
  • 类似字体文件这种隐藏在脚本、样式中的首屏关键资源,建议使用preload
  • 异步加载的模块(典型的如单页系统中的非首页)建议使用prefetch
  • 大概率即将被访问到的资源可以使用prefetch提升性能和体验

总结和踩坑:
1、preload 和 prefetch 的本质都是预加载,即先加载、后执行,加载与执行解耦。

2、preload 和 prefetch不会阻塞页面的onload

3、preload 用来声明当前页面的关键资源,强制浏览器尽快加载;而prefetch 用来声明将来可能用到的资源,在浏览器空闲时进行加载。

4、不要滥用preload 和 prefetch,需要在合适的场景中使用。

5、preload 的字体资源必须设置 crossorigin属性,否则会导致重复加载。 原因是如果不指定 crossorigin属性(即使同源),浏览器会采用匿名模式的 CORS 去 preload,导致两次请求无法共用缓存。

6、关于preload 和prefetch 资源的缓存,在 Google 开发者的一篇文章中是这样说明的:如果资源可以被缓存(比如说存在有效的 cache-control 和max-age),它被存储在HTTP 缓存(也就是disk cache)中,可以被现在或将来的任务使用;如果资源不能被缓存在 HTTP 缓存中,作为代替,它被放在内存缓存中直到被使用。

PWA 离线可访问

渐进式网络应用程序(PWA) ,是一种可以提供类似于原生应用程序(native app)体验的网络应用程序(web app)。

PWA 可以用来做很多事。其中最重要的是,在离线时应用程序能够继续运行功能。

下载依赖

npm i workbox-webpack-plugin -D

配置webpack.config.js

// webpack.config.jsconst WorkboxPlugin = require('workbox-webpack-plugin');
​
module.exports = {
    plugins: [
            new WorkboxPlugin.GenerateSW({
                  // 这些选项帮助 ServiceWorkers 快速启用
                  // 不允许遗留任何“旧的” ServiceWorkers
                  /* 更多配置详见:https://developers.google.com/web
                              /tools/workbox/modules/workbox-webpack-plugin
                  */
                
                  /*
                    1. 帮助 serviceworker 快速启动
                    2. 删除旧的 serviceWorker
                  */
                  clientsClaim: true,
                  skipWaiting: true,
​
                  //打包到本地, 默认值是'cdn' 访问的是国外cdn需要翻墙
                  importWorkboxFrom: 'local',  
                  include: [/.html$/, /.js$/, /.css$/],  //包含资源
                  exclude: [/.(png|jpg|gif|svg)/]  //排除资源
            })
        
        ]     
        
 }
​
        

注册 Service Worker

index.js
// index.js

// 处理兼容性问题
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
     navigator.serviceWorker.register('/service-								worker.js').then(registration => {
       console.log('SW registered: 成功 ', registration);
     }).catch(registrationError => {
       console.log('SW registration failed:失败 ', registrationError);
     });
    });
}

package.json ESLint配置

让其支持浏览器变量(window、navigator等全局变量)

/*
	eslint 不认识 window、navigator 全局变量
	解决: 需要修改 package.json 中的 EslintConfig 配置
		 "env": {
       		 "browser": true  // 支持浏览器端全局变量
   		 }
*/

{
  "eslintConfig": {
    "extends": "airbnb-base",
    "env": {
        "browser": true
    }
  }
}

启动一个本地服务器

npm install http-server --save-dev
$ npx http-server dist

# 或者
npm i serve
$ npx serve dist

多进程打包

js单线程,同一时间只能干一件事,排队等很久才能干下一件事,所以我们就需要使用多线程

开启电脑的多个进程同时干一件事。我们想要继续提升打包速度,其实就是要提升 js 的打包速度,而对 js 文件处理主要就是 eslint 、babel、Terser 三个工具。

我们启动进程的数量就是我们 CPU 的核数。每个进程启动大概为600ms,进程通信也有开销,不要滥用。

一般用在 babel-loader 下

下载依赖

npm install thread-loader --save -dev

   # 或者
npm i thread-loader -D

配置 Webpack.config.js

//webpack.config.js
{
            test: /.js$/,
            exclude: /node_modules/,
            use: [
                // 开启多进程打包(babel工作的时候就会开启多进程)
                /*
                	开启多进程打包是有利有弊的(合理使用)
                	进程启动大概600ms,进程通信也有开销(时间)
                	只有工作消耗时间比较长,才需要多进程
                	一般来说js代码比较多,消耗时间比较长
                	
                	启动进程数(cpu核数-1)
                	
                */
                //'thread-loader',
                //如下可做调整
                {
                    loader: 'thread-loader',
                    options: {
                    	// 进程数量可控的配置
                		workers: 2 // 进程2个
                	}
                },
                {
                    loader: 'babel-loader',
                    options: {
                      presets: [
                          [
                              '@babel/preset-env',
                              {
                                  // 按需加载
                                  useBuiltIns: 'usage',
                                  corejs: {version: 3},
                                  targets: { chrome: '60' }
                              }
                          ]
                        ],
                        // 开启babel缓存
                        // 第二次构建时,会读取之前的缓存
                        cacheDirectory: true
                     }
            	]
            }
          }
cache-loader

缓存资源,提高二次构建的速度,使用方法是将cache-loader 放在比较费时间的 loader 之前,比如 babel-loader,由于启动项目和打包项目都需要加速,所以配置在 webpack.config.js

安装依赖
npm i cache-loader -D
配置 webpack.config.js
// webpack.config.js
{
test: /.js$/,
use: [
    'cache-loader'
    'thread-loader',
    'babel-loader'
 ],
}
​

外部扩展(Externals)

externals  配置项提供了阻止将某些  import 的包 (package) 打包到  bundle  中的功能,在运行时 ( runtime) 再从外部获取这些扩展依赖(external dependencies)

externals 用于提取第三方依赖包使用 cdn资源的方式 将第三方依赖包引入项目,可以大大减少项目打包体积 

externals用法

module.exports={
    configureWebpack:congig =>{
        externals:{
            key: value
        }
    }
}
​
语法说明
  •  key  是第三方依赖库的名称 ,同  package.json 文件中的  dependencies 对象的  key 一样
"devDependencies": {
    "webpack": "^4.41.6",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.10.3",
    "workbox-webpack-plugin": "^5.0.0"
  },
  •  value 值可以是字符串、数组、对象 。应该是第三方依赖编译打包后生成的 js(要用CDN引入的 js 文件)文件,执行后赋值给  window  的全局变量名称。在控制台打印window.xxxvalue 就是 xxx

bc5.png

  1. 有些  JavaScript  运行环境可能内置了一些全局变量或者模块,例如在你的 HTML BODY  标签里通过以下代码:
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>

引入 jquery  后,全局变量  jQuery  就会被注入到网页的  JavaScript  运行环境里。就可以直 接通过  window.$ 来访问  jQuery 对象。

  1. 当运行环境内置了  jQuery  全局变量  (即在 body 中使用 script 标签引入了 jquery 框架),如果同时又使用模块化的方式安装并使用了 jQuery(npm install jquery --save),可能这个时候就会出现重复引用框架的问题。
// 我们不想这么用
// const $ = window.jQuery
 
// 而是这么用
const $ = require("jquery")
$("#content").html("<h1>hello world</h1>")

当构建后你会发现输出的 bundle.js  里包含的  jQuery  库的内容,这导致  jQuery  库出现了2次,第一次是 body 中的  script  标签加载,第二次是  bundle.js  中加载,浪费加载流量,最好是  bundle.js里不会包含  jQuery  库的内容。那这个时候就用上  Externals  的配置项了

配置 webpack.config.js

// webpack.config.js
​
const path = require('path');
​
module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './dist'),
  },
  externals: {
     /**
      * externals 对象属性解析。
      *  基本格式:
      *     '包名' : '在项目中引入的名字'
      *  
      */
      
      jquery: "jQuery"
  },
   
 /* 函数表达
    ctx.context (string): 包含引用的文件目录。
    ctc.request (string): 被请求引入的路径。
    ctx.contextInfo (object): 包含 issuer 的信息(如,layer 和 compiler)
    ctx.getResolve 5.15.0+: 获取当前解析器选项的解析函数。
    callback (function (err, result, type)): 用于指明模块如何被外部化的回调函数
    回调函数接收三个入参
        err (Error): 被用于表明在外部外引用的时候是否会产生错误。如果有错误,这将会是唯                      一被用到的参数。
        
        result (string [string] object): 描述外部化的模块。可以接受其它标准化外部                       化模块格式,(string, [string],或 object)。
        
        type (string): 可选的参数,用于指明模块的 external type(如果它没在 result         参数中被指明)。
*/
    
  externals: function(context, request, callback) {
    if (request === 'lodash') {
      return callback(null, '_');
    }
    callback();
  }
​
};

使用插件

安装依赖
npm i html-webpack-externals-plugin -D
配置webpack.config.js
//  webpack.config.jsconst HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
​
module.exports = { 
​
    plugins: [ 
    new HtmlWebpackExternalsPlugin({ 
        externals: [{ 
            module: 'vue', 
            entry:'https://lib.baomitu.com/vue/2.6.12/vue.min.js',
            global: 'Vue' 
          }]
      })
    ],
}
​
动态添加 CDN 到 index.html中
<script type="text/javascript" src="https://lib.baomitu.com/vue/2.6.12/vue.min.js"></script>

dll (动态链接库)

什么是dll?为什么需要dll?

类似 externals,会指示 webpack那些库是不参与打包的,不同的是 webpack 会单独对某些库进行单独打包,将多个库打包成一个 chunk ,可以有效避免对这些包的重复打包。

node_modules 内的某些库比较大,正常打包的话会被打包成一个文件,这样文件体积增大。通过 dll 将这些库单独拆开,打包成不同的  chunk,更有利于性能优化

dll 打包后能,webpack 运行打包后不会重复打包 第三方依赖库 (第三方库:jqueryreactvue...),提高效率。

特点: 只会在内存中编译打包,不会有任何输出

适用范围

Dll 文件里只适合放置不常改动的代码,比如说第三方库(谁也不会有事无事就升级一下第三方库吧),尤其是本身就庞大或者依赖众多的库。如果你自己整理了一套成熟的框架,开发项目时只需要在上面添砖加瓦的,那么也可以把这套框架也打包进 Dll 文件里,甚至可以做到多个项目共用这一份 Dll 文件。

所需插件

DllPlugin

此插件用于在单独的 webpack 配置中创建一个 dll-only-bundle。 此插件会生成一个名为 manifest.json 的文件,这个文件是用于让 DllReferencePlugin 能够映射到相应的依赖上。

属性描述
context(可选)manifest 文件中请求的 context (默认值为 webpack 的 context)
format (boolean = false)如果为 true,则 manifest json 文件 (输出文件) 将被格式化
name暴露出的DLL 的函数名(TemplatePaths:[fullhash] & [name] )
pathmanifest json文件的 绝对路径(输出文件)
entryOnly (boolean = true)如果为 true,则仅暴露入口 type:dll bundle的类型

DllReferencePlugin

此插件配置在 webpack 的主配置文件中,此插件会把 dll-only-bundles 引用到需要的预编译的依赖中。

属性描述
context(绝对路径) manifest (或者是内容属性)中请求的上下文
extensions用于解析 dll bundle 中模块的扩展名 (仅在使用  scope 时使用)。
manifest用于解析 dll bundle 中模块的扩展名 (仅在使用  scope 时使用)。
content (可选)请求到模块 id 的映射(默认值为 manifest.content )
name (可选)dll 暴露地方的名称(默认值为 manifest.name )(可参考externals )
scope (可选)dll 中内容的前缀
sourceType (可选)dll 是如何暴露的 (libraryTarget)
AddAssetHtmlPlugin

该插件会将给定的 JSCSS 文件添加到 Webpack 知道的文件中,并将其放入  html-webpack-plugin 注入到生成的  html 中的资产列表中。将插件添加到配置中,并为其提供一个文件路径

属性描述默认值
filepath要添加到编译和生成的 HTML文件的绝对路径必须,除非已定义 glob
glob用于查找要添加到编译中的文件的 glob。使用方法请参阅 globby 4 文档必须,除非已定义 glob
files默认情况下,资产将包含在所有文件中。如果定义了文件,资产将只包含在指定的文件球中[]
hash如果为 “true”,则会将文件的唯一哈希值追加到文件名中。这对消除缓存很有用false
includeRelatedFiles如果为 “true”,则会在编译时添加 filepath + ‘.*’ 。例如,filepath.mapfilepath.gztrue
outputPath如果设置,将用作文件的输出目录
outputPath如果设置,将用作脚本或链接标记的公共路径
typeOfAsset设置为 css,以创建链接标记,而不是脚本标记js
attributes要添加到生成标签中的额外属性。例如,在多填充脚本中添加 nomodule 就很有用。属性对象使用 key 作为属性名称,使用 value 作为属性值。如果 value 仅为 true,则不会添加任何值{}
安装依赖
npm i add-asset-html-webpack-plugin -D

配置Dll文件

  1. 运行  webpack  时,默认查找  webpack.config.js  配置文件,需要运行下面的指令。
npx webpack --config webpack.dll.config.js

2. 在 webpack.config.js  的同名文件创建一个  webpack.dll.js 文件,并在其中对  jquery 进行单独打包。

    // webpack.dll.config.js/**
     * 使用dll技术,对某些库(第三方库:jQuery,vue, react...)进行单独打包
     * 使用 DllPlugin 插件 配置
     */const DllPlugin = require('webpack/lib/DllPlugin');
    const { resolve } = require('path');
    const webpack = require('webpack');
    ​
    module.exports = {
        
      // 入口文件
      entry: {
        vendors: ["jquery"],
        // jquery: ['jquery']
      },
       
      // 输出
      output: {
        filename: '[name].dll.js',
        path: resolve(__dirname,'dll'),
        /*
        存放相关的dll文件的全局变量名称,比如对于jquery来说的话就是 _dll_jquery, 在       前面加 _dll
        是为了防止全局变量冲突。
       */
        library: '[name]_[hash]',
        clean: true,
      },
        
      // 插件配置
      plugins: [
        // 用于清除上次生成的包
        new CleanWebpackPlugin(),  
        
        // 打包生成一个manifest.json提供和jquery的映射
        new DllPlugin({
           /*
            该插件的name属性值需要和 output.library保存一致,该字段值,也就是输出的           manifest.json文件中name字段的值。
            比如在jquery.manifest文件中有 name: '_dll_jquery'
         */
            
         name: '_dll_[name]',
    ​
         /* 生成manifest文件输出的位置和文件名称 */
         path: path.join(__dirname, 'lib/', '[name].manifest.json')
    ​
        })
      ],
        
      // 模式
      mode: 'production'
    }
    ​

配置 webpack.config.js

告诉  webpack 哪些库不参与打包,并使用插件将 dll 已经打包好的  jquery 文件进行引入,这样以后  jquery 都不需要参与打包了,因为已经打包好了,我们只需要引入即可。

//  webpack.config.js
   
const HtmlWebpackPlugin = require('html-webpack-plugin');
   const {resolve} = require('path');
   const webpack = require('webpack');
   const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
   
   module.exports = {
     entry: './src/index.js',
       
       // 项目中用到的依赖库文件
      // echartsbt: ['echarts']
     output: {
       filename: 'built.js',
       path: resolve(__dirname,'build')
       // 在生成文件之前清空 output 目录
       clean: true,
     },
     module: {
       rules: [
   
       ]
     },
     plugins: 
       
        // 复制一份HTML文件,并自动引入打包资源(js/css)
        new HtmlWebpackPlugin({
          template: "./src/index.html",
        }),
​
        // 告诉webpack 哪些库不参与打包
        new DllReferencePlugin({
          // echarts 映射到json文件上去
         manifest: resolve(__dirname,'dll/manifest.json')
       }),
         
       // 将某个文件打包输出去,并在html中自动引入该资源
       //将生成的dll文件加入到index.html中
       new AddAssetHtmlWebpackPlugin({
         filepath: resolve(__dirname,'dll/jquery.js')
       })
     ],
         
        // 模式
        // mode: "development",
        mode: "production",
   }
   

性能优化总结

开发环境性能优化

  • 优化打包构建速度

    • HMR 开启HMR功能
  • 优化代码调试

    • source-map 配置 devtool: ‘source-map’

生产环境性能优化

  • 优化打包构建速度

    • oneOf 默认情况下,假设设置了多个 loader,每一个文件都得通过这多个 loader处 理(过一遍),浪费性能,使用 oneOf 找到了就能直接用,提升性能。

    • babel缓存 当一个 js 文件发生变化时,其它 js 资源不用变

    • 多进程打包 开启多进程打包,主要处理js文件(babel-loader干的活久),

      进程启动大 概为600ms,只有工作消耗时间比较长,才需要多进程打包, 提升打包速度

    • externals

    • dll 是将第三方库打包成多个bundle,从而进行速度优化

  • 优化代码运行的性能

    • 缓存(hash-chunkhash-contenthash)
    • tree shaking
    • code split 将第三方库都打包成一个bundle,这样体积过大,会造成打包速度慢
    • 懒加载/预加载
    • pwa