自动化构建工具——14.webpack 性能优化

webpack性能优化

一、开发环境性能优化

最关键的点就是希望打包速度更快一些就够了

1.1.优化打包构建速度

  • HMR

1.1.1.HMR

目录结构复用5.开发环境配置中08.开发环境配置

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

  • 作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块),其他模块使用缓存,极大提升构建速度。
1.1.1.1.运行npx webpack-dev-server并修改样式文件

当我们修改css样式文件的时候,js文件没有变化,但是也被重新加载了一次,所以看似只修改了css文件,但实际上吧js文件也重新打包了一次。这代表着假设有100个模块,100个样式模块,只要修改其中的某一个模块,整个200个模块都要重新打包,这会严重影响速度。所以我们想要实现只有一个模块发生变化,就只修改这一个模块就够了,其他模块理应不变。这需要webpack的HMR功能来实现。

1.1.1.2.开启HMR功能
  • 开启方法:在配置文件中的devServer下进行开启即可

webpack.config.js

const {
  resolve
} = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      // loader的配置
      {
        // 处理less资源
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          'less-loader'
        ]
      },
      {
        // 处理css资源
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
        ]
      },
      {
        // 处理图片资源
        test: /\.(jpg|png|gif)$/,
        loader: 'url-loader',
        options: {
          limit: 8 * 1024,
          name: '[hash:10].[ext]',
          // 关闭ES6模块化
          esModule: false,
          // 将图片输出到imgs目录下
          outputPath: 'imgs'
        }
      },
      {
        // 处理html中的img资源
        test: /\.html$/,
        loader: 'html-loader',
      },
      {
        // 处理其他资源
        exclude: /\.(css|js|html|less|jpg|png|gif)$/,
        loader: 'file-loader',
        options: {
          name: '[hash:10].[ext]',
          outputPath: 'media'
        }
      }
    ]
  },
  plugins: [
    // plugins的配置
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  mode: 'development',
  devServer: {
    contentBase: resolve(__dirname, 'build'),
    compress: true,
    port: 3000,
    open: true,
    // 开启HMR功能
    // 当修改了webpack配置,新配置想要生效,必须重新启动webpack服务
    hot: true
  }
}

注:当修改了webpack配置,新配置想要生效,必须重新启动webpack服务

1.1.1.3.重启webpack服务并修改样式文件查看效果

自动化构建工具——14.webpack 性能优化_第1张图片

可以看到此时样式文件发生变化,就只会更新样式文件而不是全部重新打包

1.1.1.4.分别对各类文件进行修改

发现以下结果:

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

  • js文件:默认没有HMR功能

    • 解决:需要修改js代码,添加支持HMR功能的代码

    src->js->print.js

    console.log('print.js被加载了')
    
    function print() {
      const content = 'hello print'
      console.log(content)
    }
    
    export default print
    

    src->js->index.js

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

    注意:HMR功能对js的处理,只能处理非入口的js文件

    此时修改print.js文件内容可以看到只会更新print.js文件而不是全部重新打包
    自动化构建工具——14.webpack 性能优化_第2张图片

  • html文件:默认不能使用HMR功能,同时会导致问题:html文件不能热更新了(不用做HMR功能)

    • 解决:修改entry入口,将html文件引入

    修改配置稳健的entry部分

    entry: ['./src/js/index.js','./src/index.html'],
    

1.2.优化代码调试

  • source-map

1.2.1.source-map

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

作用:优化代码调试功能

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

开启方式:在配置文件下的devtool进行配置,例如devtool: 'source-map'

  • 内联和外部的区别:
    • 1.外部生成了文件,内联没有 2. 内联构建速度更快
1.2.1.1.各形式值含义
  • source-map
    • 外部
    • 能找到错误代码准确信息,和源代码的错误位置
  • inline-source-map
    • 内联
    • 只生成一个内联source-map
    • 能找到错误代码准确信息,和源代码的错误位置
  • hidden-source-map
    • 外部
    • 提示错误代码的错误原因,但是没有错误位置
    • 不能追踪到源代码错误,只能提示到构建后代码的错误位置
  • eval-source-map
    • 内联
    • 每一个文件都生成对应的source-map,都在eval中
    • 能找到错误代码准确信息,和源代码的错误位置
  • nosources-source-map
    • 外部
    • 能找到错误代码准确信息,但是没有任何源代码信息
  • cheap-source-map

    • 外部
    • 能找到错误代码准确信息,和源代码的错误位置
    • 只能精确到行,而其他(例如source-map)是可以精确到行和列的
  • cheap-module-source-map

    • 外部
    • 能找到错误代码准确信息,和源代码的错误位置
    • module会将loader的source map加入
1.2.1.2.其在开发环境中的使用

目的:速度快一点,调试更友好

  • 速度快(eval>inline>cheap)

    1. eval-cheap-source-map(因为cheap只精确到行)

    2. eval-source-map

  • 调试更友好

    1. source-map
    2. cheap-module-source-map
    3. cheap-source-map
  • 综合起来最终

    • eval-source-map:最友好,脚手架默认使用
    • eval-cheap-module-source-map:性能更好,打包信息更加完整
1.2.1.3.其在生产环境中的使用

考虑的问题:源代码要不要隐藏?调试要不要更友好?

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

  • 源代码隐藏

    • nosources-source-map 全部隐藏
    • hidden-source-map 只隐藏源代码,会提示构建后代码错误
  • 综合考虑

    • source-map:调试更友好,脚手架默认使用
    • cheap-module-source-map:速度更快
  • source-map

二、生产环境性能优化

对于生产环境优化需要考虑的点则非常的复杂,面面俱到,目的是让我们将来上线的代码性能达到最好,从而让用户体验好,产品就更好。

2.1.优化打包构建速度

  • oneOf
  • babel缓存
  • 多进程打包
  • externals
  • dll

2.1.1.oneOf

一般来说,每处理一种类型资源,所有配置文件中的loader都会被过一遍,这样就不太好,通过oneOf能够让我们处理的性能更好

注意:不能有两个配置处理同一种类型的文件,如果有这种情况,则将其中一个配置提取出去即可

  • 作用:优化打包构建速度
  • 使用方法:将处理不同类型文件loader规则部分的代码放到oneOf选项的数组中

配置文件

const { resolve } = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 设置node.js环境变量:决定使用browserlist的哪个环境
process.env.NODE_ENV = 'development'
const commonCssLoader = [
  // 提取成单独文件
  MiniCssExtractPlugin.loader,
    'css-loader',
    // 对样式进行兼容性处理
    // use数组中loader执行顺序:从右到左,从下到上依次执行,所以样式兼容性处理放在coo-loader后面
    {
      // 还需要在package.json中对browserlist进行设置,且如果需要使用开发环境配置,则要设置node.js环境变量,不然browserlist将默认使用production下的配置
      loader: 'postcss-loader',
      options: {
        ident: 'postcss',
        plugins: () => [require('postcss-preset-env')()]
      }

    }
]
module.exports = {
  entry: './src/js/index.js',
  output: {
    // 文件名
    filename: 'js/bulit.js',
    // 输出的文件目录
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      // 处理js文件
      {
        // 在package.json文件中eslintConfig配置使用airbnb规则
        test: /\.js$/,
        exclude: /node_modules/, // 排除检测node_modules的内容
        // 优先执行该loader,确保两个loader不会产生冲突
        enforce: 'pre',
        loader: 'eslint-loader',
        options: {
          // 自动修复检查到的问题
          fix: true
        }
      },
      {
        // 以下loader只会匹配一个
        // 注意:不能有两个配置处理同一种类型的文件
        oneOf: [
          // 处理css样式文件
          {
            test: /\.css$/,
            use: [...commonCssLoader]
          },
          // 处理less文件
          {
            test: /\.less$/,
            use: [
              ...commonCssLoader,
              // 将less文件编译成css文件
              // 需要下载less-loader和less
              'less-loader'
            ]
          },
          // 对js做兼容性处理
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              // 预设
              presets: [
                '@babel/preset-env',
                {
                  // 实现按需加载
                  useBuiltIns: 'usage',
                  // 指定core-js版本,可以到package.json查看
                  corejs: {
                    version: 3
                  },
                  // 指定兼容哪些版本的浏览器
                  targets: {
                    chrome: '60',
                    firefox: '50',
                    ie: '9'
                  }
                }
              ]
            }
          },
          // 处理图片文件
          {
            test: /\.(jpg|png|gif)/,
            loader: 'url-loader',
            options: {
              limit: 8 * 1024,
              name: '[hash:10].[ext]',
              // 输出路径
              outputPath: 'imgs',
              esModule: false,
            }
          }, {
            test: /\.html$/,
            // 处理html文件中的img图片(负责引入img,从而能被url-loader进行处理)
            loader: 'html-loader'
          },
          // 处理其它文件
          {
            // 排除以下几种文件
            exclude: /\.(js|css|less|html|jpg|png|gif)/,
            loader: 'file-loader',
            options: {
              outputPath: 'media'
            }
          }

        ]
      }
      
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      // 对输出的文件进行重命名
      filename: 'css/built.css'
    }),
    // 压缩css
    new OptimizeCssAssetsWebpackPlugin(),
    // 处理html文件
    new HtmlWebpackPlugin({
      // 以指定html文件为模板创建新的html文件
      template: './src/index.html',
      // 压缩html文件
      minify: {
        // 压缩空格
        collapseWhitespace: true,
        // 去除注释
        removeComments: true
      }
    })
  ],
  // 生产环境下js会自动压缩
  mode: 'production'
}

2.1.2.babel缓存

在写代码时,js的代码永远是最多的

  • 对babel进行缓存的原因:因为babel需要对我们写的js代码进行编译处理,编译成浏览器能够识别的语法,就是所谓的js兼容性处理
  • 作用:让第二次打包更快
  • 过程:babel先把所有编译后的js文件进行缓存处理,这样在发现文件没有变化的时候就直接使用缓存,而不会再重新构建一次
  • 配置:在配置文件的对js做兼容性处理部分options属性中直接添加cacheDirectroy选项并设置为true来开启babel缓存
// 对js做兼容性处理
{
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
    options: {
        // 预设
        presets: [
            '@babel/preset-env',
            {
                // 实现按需加载
                useBuiltIns: 'usage',
                // 指定core-js版本,可以到package.json查看
                corejs: {
                    version: 3
                },
                // 指定兼容哪些版本的浏览器
                targets: {
                    chrome: '60',
                    firefox: '50',
                    ie: '9'
                }
            }
        ],
        // 开启babel缓存
        // 第二次构建时,会读取之前的缓存
        cacheDirectroy: true
    }
}

2.1.3.多进程打包

通过多进程的方式优化打包速度

  • 适用场景:针对消耗时间较长的任务(大于600ms)

  • 需要的工具:thread-loader

npm i thread-loader -D
  • 使用:将thread-loader放在某个loader后面,就会对这个loader开启多进程打包

配置文件

const {
  resolve
} = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const WorkboxWebpackPlugin = require('workbox-webpack-plugin')

// 设置node.js环境变量:决定使用browserlist的哪个环境
process.env.NODE_ENV = 'production'

// 复用loader
const commonCssLoader = [
  // 提取成单独文件
  MiniCssExtractPlugin.loader,
  'css-loader',
  // 对样式进行兼容性处理
  // use数组中loader执行顺序:从右到左,从下到上依次执行,所以样式兼容性处理放在coo-loader后面
  {
    // 还需要在package.json中对browserlist进行设置,且如果需要使用开发环境配置,则要设置node.js环境变量,不然browserlist将默认使用production下的配置
    loader: 'postcss-loader',
    options: {
      ident: 'postcss',
      plugins: () => [require('postcss-preset-env')()]
    }

  }
]

module.exports = {
  entry: './src/js/index.js',
  output: {
    // 文件名
    filename: 'js/built.[contenthash:10].js',
    // 输出的文件目录
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      // 处理js文件
      {
        // 在package.json文件中eslintConfig配置使用airbnb规则
        test: /\.js$/,
        exclude: /node_modules/, // 排除检测node_modules的内容
        // 优先执行该loader,确保两个loader不会产生冲突
        enforce: 'pre',
        loader: 'eslint-loader',
        options: {
          // 自动修复检查到的问题
          fix: true
        }
      },
      {
        // 以下loader只会匹配一个
        // 注意:不能有两个配置处理同一种类型的文件
        oneOf: [
          // 处理css样式文件
          {
            test: /\.css$/,
            use: [...commonCssLoader]
          },
          // 处理less文件
          {
            test: /\.less$/,
            use: [
              ...commonCssLoader,
              // 将less文件编译成css文件
              // 需要下载less-loader和less
              'less-loader'
            ]
          },
          // 对js做兼容性处理
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              /**
               * 开启多进程打包
               * 进程启动大概为600ms,进程通信也有开销
               * 只有工作消耗时间比较长,才需要多进程打包
               */
              {
                loader: 'thread-loader',
                options: {
                  workers: 2 // 进程数
                }
              },
              {
                loader: 'babel-loader',
                  options: {
                    // 预设
                    presets: [
                      [
                        '@babel/preset-env',
                        {
                          // 实现按需加载
                          useBuiltIns: 'usage',
                          // 指定core-js版本,可以到package.json查看
                          corejs: {
                            version: 3
                          },
                          // 指定兼容哪些版本的浏览器
                          targets: {
                            chrome: '60',
                            firefox: '50',
                            ie: '9'
                          }
                        }
                      ]
                    ],
                    // 开启babel缓存
                    // 第二次构建时,会读取之前的缓存
                    cacheDirectory: true
                  }
              }
            ]
            
          },
          // 处理图片文件
          {
            test: /\.(jpg|png|gif)/,
            loader: 'url-loader',
            options: {
              limit: 8 * 1024,
              // 给图片重命名
              // [hash: 10]取图片的hash的前10位
              // [ext]取文件原来扩展名
              name: '[hash:10].[ext]',
              // 输出路径
              outputPath: 'imgs',
              esModule: false,
            }
          }, 
          {
            test: /\.html$/,
            // 处理html文件中的img图片(负责引入img,从而能被url-loader进行处理)
            loader: 'html-loader'
          },
          // 处理其它文件
          {
            // 排除以下几种文件
            exclude: /\.(js|css|less|html|jpg|png|gif)/,
            loader: 'file-loader',
            options: {
              outputPath: 'media'
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      // 对输出的文件进行重命名
      filename: 'css/built.[contenthash:10].css'
    }),
    // 压缩css
    new OptimizeCssAssetsWebpackPlugin(),
    // 处理html文件
    new HtmlWebpackPlugin({
      // 以指定html文件为模板创建新的html文件
      template: './src/index.html',
      // 压缩html文件
      minify: {
        // 压缩空格
        collapseWhitespace: true,
        // 去除注释
        removeComments: true
      }
    }),
    new WorkboxWebpackPlugin.GenerateSW({
      /**
       * 1.帮助serviceworker快速启动
       * 2.删除就得serviceworker
       * 
       * 会生成一个serviceworker 配置文件
       */
      clientsClaim: true,
      skipWaiting: true
    })
  ],
  // 生产环境下js会自动压缩
  mode: 'production',
  devtool: 'source-map'
}
  • 问题:进程之间的启动和开销比较昂贵(进程启动大概为600ms,进程通信也有开销),所以不能滥用,只有工作消耗时间比较长,才需要多进程打包(多数用于有较多js文件需要处理的情况)

2.1.4.externals

  • 作用:防止将某一些包打包到最终输出的bundle中。

  • 用法:当开发时发现有些包是通过cdn链接引入的,则可以通过配置externals来拒绝打包,再在html文件中通过script标签的方式将这些包引入即可

  • 修改配置文件(演示以jQuery为例)

const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'built.js',
    path: resolve(__dirname, 'build')
  },
  plugins: [
    new HtmlWebpackPlugin({
      // 复制'./src/index.html'文件,并自动引入打包输出的所有资源(JS/CSS)
      template: './src/index.html'
    })
  ],
  mode:'production',
  externals: {
    // 拒绝jQuery被打包进来
    // 忽略的库名 -- npm包名
    jquery: 'jQuery'
  }
}
  • 修改index.js源代码
import $ from 'jquery'

console.log($)
  • 手动引进cdn链接

获取cdn链接网址:https://www.bootcdn.cn/

index.html源代码


<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <h1 id="title">hello htmlh1>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.12.4/jquery.min.js">script>
body>
html>
  • 运行webpack
    自动化构建工具——14.webpack 性能优化_第3张图片
    根据构建后体积大小可以看出此时并未将jQuery一起打包

  • 运行构建后index.html
    自动化构建工具——14.webpack 性能优化_第4张图片

可以看到index.js中引入的jQuery是可以生效的

2.1.5.dll

动态连接库,类似externals一样会指示webpack哪些库是不参与打包的,不同的是dll会单独对某些库进行打包,把库先打包好,后面就直接用,不需要在再打包

  • 意义:正常情况下node_modules里面所有的包会被打包成一个文,但是第三方库有非常多,如果全部打包成一个文件,这样文件体积太大,所以通过dll技术我们可以将这些库单独拆开来打包成不同的文件,这样更加有利于我们的性能优化。

以下以jquery为例进行演示

  • 需要下载的插件:add-asset-html-webpack-plugin
npm i add-asset-html-webpack-plugin -D
  • 定义webpack.dll.js文件
/**
 * 使用dll技术,对某些库(第三方库:jquery、react、vue……)进行单独打包
 * 当运行webpack时,默认查找webpack.config.js配置文件
 * 需求:需要运行webpack.dll.js文件
 *    webpack --config webpack.dll.js
 */

const { resolve } = require('path')
const webpack = require('webpack')

module.exports = {
  entry: {
    // 最终打包生成的[name] --> jquery
    // ['jquery'] --> 要打包的库时jquery
    jquery: ['jquery']
  },
  output: {
    filename: '[name].js',
    path:resolve(__dirname, 'dll'),
    library: '[name]_[hash]',  // 打包的库里面向外暴露出去的内容叫什么名字
  },
  plugins: [
    // 打包生成一个manifest.json文件(提供和jquery映射)
    new webpack.DllPlugin({
      name: '[name]_[hash]',   // 映射库的暴露的内容名称
      path: resolve(__dirname, 'dll/manifest.json')   // 输出文件路径
    })
  ],
  mode: 'production'
}
  • 运行webpack --config webpack.dll.js
    自动化构建工具——14.webpack 性能优化_第5张图片

生成一个jquery.js和manifest.json(提供映射关系)文件
自动化构建工具——14.webpack 性能优化_第6张图片

  • 修改webpack.config.js配置文件
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'built.js',
    path: resolve(__dirname, 'build')
  },
  plugins: [
    new HtmlWebpackPlugin({
      // 复制'./src/index.html'文件,并自动引入打包输出的所有资源(JS/CSS)
      template: './src/index.html'
    }),
    // 告诉webpack哪些库不参与打包,同时使用时名称也得变
    new webpack.DllReferencePlugin({
      manifest: resolve(__dirname, 'dll/manifest.json')
    }),
    // 将某个文件打包输出去,并在html中自动引入该资源
    new AddAssetHtmlWebpackPlugin({
      filepath: resolve(__dirname, 'dll/jquery.js')
    })
  ],
  mode: 'production'
}
  • 运行webpack
    自动化构建工具——14.webpack 性能优化_第7张图片

2.2.优化代码运行的性能

  • 持久化缓存
  • tree shaking
  • code split
  • 懒加载/预加载
  • pwa

2.2.1.持久化缓存(hash-chunkhash-contenthash)

通过输出文件命名依据(hsah值)来实现,开启babel缓存之后,每次加载在发现文件没有变化的时候就直接使用缓存,而不会再重新构建一次,所以只要维护hsah值的变化就能够实现控制某个文件是否有更新。

作用:为了使当改变某一文件时,其余文件不会一起更新,让代码上线运行缓存更好使用(上线代码性能优化)

共有以下三种hash值:

  1. hash:每次webpack构建时会生成一个唯一的hash值

    • 问题:因为js和css同时使用一个hash值,如果重新打包,会导致所有缓存失效。(可能却只改动了一个文件)
      自动化构建工具——14.webpack 性能优化_第8张图片
  2. chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk(同一个入口),那么hash值就一样。

    • 问题:js和css的hash值还是一样的因为css是在js中被引入的,所以属于一个chunk
      自动化构建工具——14.webpack 性能优化_第9张图片
  3. contenthash:根据文件的内容生成hash值。不同文件的hash值一定不一样,可以对资源进行更有效的控制。
    自动化构建工具——14.webpack 性能优化_第10张图片

使用第三种方法修改js文件源代码,重新构建,对比前后发现只有js文件的hash值发生了变化,其余不变,同理修改css文件源代码也是如此。
自动化构建工具——14.webpack 性能优化_第11张图片

这样就可以实现没有修改的文件在加载时直接使用缓存,有修改(文件hash值发生变化)就更新该文件。

使用第三种hash值(有文件输出的地方使用)的配置文件:

const {
  resolve
} = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

// 设置node.js环境变量:决定使用browserlist的哪个环境
process.env.NODE_ENV = 'production'

// 复用loader
const commonCssLoader = [
  // 提取成单独文件
  MiniCssExtractPlugin.loader,
  'css-loader',
  // 对样式进行兼容性处理
  // use数组中loader执行顺序:从右到左,从下到上依次执行,所以样式兼容性处理放在coo-loader后面
  {
    // 还需要在package.json中对browserlist进行设置,且如果需要使用开发环境配置,则要设置node.js环境变量,不然browserlist将默认使用production下的配置
    loader: 'postcss-loader',
    options: {
      ident: 'postcss',
      plugins: () => [require('postcss-preset-env')()]
    }

  }
]

module.exports = {
  entry: './src/js/index.js',
  output: {
    // 文件名
    filename: 'js/built.[contenthash:10].js',
    // 输出的文件目录
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      // 处理js文件
      {
        // 在package.json文件中eslintConfig配置使用airbnb规则
        test: /\.js$/,
        exclude: /node_modules/, // 排除检测node_modules的内容
        // 优先执行该loader,确保两个loader不会产生冲突
        enforce: 'pre',
        loader: 'eslint-loader',
        options: {
          // 自动修复检查到的问题
          fix: true
        }
      },
      {
        // 以下loader只会匹配一个
        // 注意:不能有两个配置处理同一种类型的文件
        oneOf: [
          // 处理css样式文件
          {
            test: /\.css$/,
            use: [...commonCssLoader]
          },
          // 处理less文件
          {
            test: /\.less$/,
            use: [
              ...commonCssLoader,
              // 将less文件编译成css文件
              // 需要下载less-loader和less
              'less-loader'
            ]
          },
          /**
           * 正常来说,一个文件只能被一个loader处理。
           * 当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序
           *    先执行eslint 再执行 babel
           *      1. 因为eslint是执行语法检查的,如果语法出现错误,执行babel也没有意义
           *      2. babel会将ES6语法转换为ES5语法,一旦经过babel转换之后再进行eslint检查就会报语法错误
           */
          // 对js做兼容性处理
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              // 预设
              presets: [
                [
                  '@babel/preset-env', 
                  {
                    // 实现按需加载
                    useBuiltIns: 'usage',
                    // 指定core-js版本,可以到package.json查看
                    corejs: {
                      version: 3
                    },
                    // 指定兼容哪些版本的浏览器
                    targets: {
                      chrome: '60',
                      firefox: '50',
                      ie: '9'
                    }
                  }
                ]
              ],
              // 开启babel缓存
              // 第二次构建时,会读取之前的缓存
              cacheDirectory: true
            }
          },
          // 处理图片文件
          {
            test: /\.(jpg|png|gif)/,
            loader: 'url-loader',
            options: {
              // 图片大小小于8kb,就会被base64处理(8~12kb)
              // 优点:减少请求数量(减轻服务器压力)
              // 缺点:图片体积会更大(文件请求速度更慢)
              limit: 8 * 1024,
              // 给图片重命名
              // [hash: 10]取图片的hash的前10位
              // [ext]取文件原来扩展名
              name: '[hash:10].[ext]',
              // 输出路径
              outputPath: 'imgs',
              /**
               * 问题:因为url-loader默认使用ES6模块化解析,而html-loader引入图片是commonjs
               * 解析时会出问题:[object Module]
               * 解决:关闭url-loader的ES6模块化,使用commonjs解析
               */
              esModule: false,
            }
          }, 
          {
            test: /\.html$/,
            // 处理html文件中的img图片(负责引入img,从而能被url-loader进行处理)
            loader: 'html-loader'
          },
          // 处理其它文件
          {
            // 排除以下几种文件
            exclude: /\.(js|css|less|html|jpg|png|gif)/,
            loader: 'file-loader',
            options: {
              outputPath: 'media'
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      // 对输出的文件进行重命名
      filename: 'css/built.[contenthash:10].css'
    }),
    // 压缩css
    new OptimizeCssAssetsWebpackPlugin(),
    // 处理html文件
    new HtmlWebpackPlugin({
      // 以指定html文件为模板创建新的html文件
      template: './src/index.html',
      // 压缩html文件
      minify: {
        // 压缩空格
        collapseWhitespace: true,
        // 去除注释
        removeComments: true
      }
    })
  ],
  // 生产环境下js会自动压缩
  mode: 'production',
  devtool: 'source-map'
}

2.2.2.tree shaking

目的:去除应用程序中没有使用的代码,让代码体积更小

使用前提:

  1. 必须使用ES6模块化
  2. 开启production环境

作用:减少代码体积,从而请求速度更快,加载速度也快

在package.json中配置"sideEffects":

  • “sideEffects”:false 所有代码都没有副作用(都可以进行tree shaking)
    • 问题:可能会把直接引入但没有使用的css/@babel/polyfill(副作用)文件去掉
  • “sideEffects”:[".css"] 不会对css文件进行处理,同理可以将不需要处理的文件(例如".less")放入数组中

2.2.3.code split

代码分割就是将我们打包的一个chunk(打包输出的一个文件)分割成多个文件,这样就可以去实现各项功能(并行加载提高速度、按需加载)

2.2.3.1.通过入口(多入口/单入口)的方式拆分文件

通过指定入口(entry)的方式实现拆分

  • 修改配置文件:
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // 单入口
  // entry: './src/js/index.js',
  entry: {
    // 多入口:有一个入口输出,最终输出就会有一个bundle
    main: './src/js/index.js',
    test: './src/js/test.js'
  },
  output: {
    // [name]:取文件名
    filename: 'js/[name].[contenthash:10].js',
    // 输出的文件目录
    path: resolve(__dirname, 'build')
  },
  plugins: [
    // 处理html文件
    new HtmlWebpackPlugin({
      // 以指定html文件为模板创建新的html文件
      template: './src/index.html',
      // 压缩html文件
      minify: {
        // 压缩空格
        collapseWhitespace: true,
        // 去除注释
        removeComments: true
      }
    })
  ],
  // 生产环境下js会自动压缩
  mode: 'production'
}
  • 应用情况:
    • 单页面应用配置一般对应单入口
    • 多页面应用配置一般对应多入口
  • 存在问题:很难去指定多个入口(入口数很多的时候),修改入口显得很麻烦。
2.2.3.2.将公共文件和node_modules中代码单独打包

为了解决当多个入口文件中都使用了一些公共的文件,打包时每个入口文件都将该公共文件一起打包,导致在加载时这些公共文件被重复加载的问题。

  • 方法:在配置文件中添加optimization配置
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // 单入口
  // entry: './src/js/index.js',
  entry: {
    // 多入口:有一个入口输出,最终输出就会有一个bundle
    main: './src/js/index.js',
    test: './src/js/test.js'
  },
  output: {
    // [name]:取文件名
    filename: 'js/[name].[contenthash:10].js',
    // 输出的文件目录
    path: resolve(__dirname, 'build')
  },
  plugins: [
    // 处理html文件
    new HtmlWebpackPlugin({
      // 以指定html文件为模板创建新的html文件
      template: './src/index.html',
      // 压缩html文件
      minify: {
        // 压缩空格
        collapseWhitespace: true,
        // 去除注释
        removeComments: true
      }
    })
  ],
  /**
   * 1.可以将node_modules中代码单独打包成一个chunk最终输出
   * 2.自动分析多入口chunk中,有没有公共文件。如果有会打包成单独一个chunk
   */
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  // 生产环境下js会自动压缩
  mode: 'production'
}
  • 作用:
    • 可以将node_modules中代码单独打包成一个chunk最终输出
    • 自动分析多入口chunk中,有没有公共文件。如果有会打包成单独一个chunk
2.2.3.3.import动态导入语法

实际开发中,还是单页面应用程序比较多,多入口使用还是较少,但是在单入口情况下,方法2只能实现第一个功能,但是单入口情况下我们仍希望能够实现文件拆分的功能,因此一般情况下使用该方法。

  • 作用:通过js代码,将某个文件单独打包

  • 使用:将原先入口文件中的引用语法修改为以下动态导入语法,以希望将test.js文件单独打包为例

    • test.js
    export function mul(x, y) {
      return x * y;
    }
    
    export function count(x, y) {
      return x - y;
    }
    
    • 入口文件index.js
    function sum(...args) {
      return args.reduce((p, c) => p + c, 0);
    }
    
    /**
     * 通过js代码,让某文件被单独打包成一个chunk
     */
    
     import(/* webpackChunkName: 'test' */'./test')
      .then((result) => {
        // 文件加载成功
        // eslint-disable-next-line
        console.log(result)
      })
      .catch(() => {
        // eslint-disable-next-line
        console.log('文件加载失败')
      })
    
    // eslint-disable-next-line
    console.log(sum(1, 2, 3))
    
    • 配置文件
    const { resolve } = require('path')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
      // 单入口
      entry: './src/js/index.js',
      output: {
        // [name]:取文件名
        filename: 'js/[name].[contenthash:10].js',
        // 输出的文件目录
        path: resolve(__dirname, 'build')
      },
      plugins: [
        // 处理html文件
        new HtmlWebpackPlugin({
          // 以指定html文件为模板创建新的html文件
          template: './src/index.html',
          // 压缩html文件
          minify: {
            // 压缩空格
            collapseWhitespace: true,
            // 去除注释
            removeComments: true
          }
        })
      ],
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      },
      // 生产环境下js会自动压缩
      mode: 'production'
    }
    
    • 运行webpack

自动化构建工具——14.webpack 性能优化_第12张图片
可以看到在单入口的情况下仍能将文件进行拆分

2.2.4.懒加载/预加载(js)

两者针对的都是js文件

配置文件

const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // 单入口
  entry: './src/js/index.js',
  output: {
    // [name]:取文件名
    filename: 'js/[name].[contenthash:10].js',
    // 输出的文件目录
    path: resolve(__dirname, 'build')
  },
  plugins: [
    // 处理html文件
    new HtmlWebpackPlugin({
      // 以指定html文件为模板创建新的html文件
      template: './src/index.html',
      // 压缩html文件
      minify: {
        // 压缩空格
        collapseWhitespace: true,
        // 去除注释
        removeComments: true
      }
    })
  ],
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  // 生产环境下js会自动压缩
  mode: 'production'
}
2.2.4.1.懒加载

懒加载相当于延迟加载,就是当触发某些条件的时候才会加载,而不是一上来就加载。利用代码分割,将import动态导入语法放在一个异步的回调函数中,当满足异步函数触发的条件,再去懒加载代码

  • 前提条件:进行代码的分割

注:第二次加载直接读取缓存,不会存在重复加载的问题

console.log('index.js文件被加载了')

// import { mul } from './test'

document.getElementById('btn').onclick = function() {
  // 懒加载
  import( /* webpackChunkName: 'test' */ './test')
    .then(({ mul }) => {
      // 文件加载成功
      // eslint-disable-next-line
      console.log(mul(4, 5))
    })
    .catch(() => {
      // eslint-disable-next-line
      console.log('文件加载失败')
    })
}
2.2.4.2.预加载

会在使用之前,提前加载js文件

  • 与正常加载相比:

    • 正常加载:可以认为是并行加载(同一时间加载多个文件)
    • 预加载:等其他资源加载完毕,浏览器空闲了再偷偷加载资源,更加灵活
  • 与懒加载相比

    • 懒加载:一定要等到要用的时候才加载,这时如果要加载的文件体积比较大,那么就要等一段较长的时间才会有反应,就会造成用户点击按钮之后出现延迟的现象,第二次之后就没问题了
    • 预加载:会偷偷帮你加载好,用户点击的时候其实已经加载好了,但是同时又不会阻塞其他资源的加载,它会等其他资源加载完毕。
  • 问题:兼容性较差,只能在一些高版本的浏览使用,在移动端或IE浏览器会有相当大的兼容性问题,所以需要慎用

console.log('index.js文件被加载了')

// import { mul } from './test'

document.getElementById('btn').onclick = function() {
  // 懒加载
  // 预加载  Prefetch:会在使用之前,提前加载js文件
  // 正常加载可以认为是并行加载( 同一时间加载多个文件)
  // 预加载: 等其他资源加载完毕, 浏览器空闲了再偷偷加载资源, 更加灵活
  import( /* webpackChunkName: 'test', webpackPrefetch: true */ './test')
    .then(({ mul }) => {
      // 文件加载成功
      // eslint-disable-next-line
      console.log(mul(4, 5))
    })
    .catch(() => {
      // eslint-disable-next-line
      console.log('文件加载失败')
    })
}

2.2.5.pwa

作用:帮助我们让我们的网页像APP应用程序一样离线也可以访问,性能也更好,也称为渐进式网络开发应用程序。

存在问题:因为兼容性问题,普及还需要一定时间。

  • 需要的库:workbox
npm i workbox -D
2.2.5.1.基本配置
  • 修改配置文件

    • 引入workbox-webpack-plugin插件
    • 在plugins部分进行配置
    const {
      resolve
    } = require('path')
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
    
    /**
     * PWA:渐进式网络开发应用程序(离线可访问)
     *  workbox--> workbox-webpack-plugin
     */
    
    // 设置node.js环境变量:决定使用browserlist的哪个环境
    process.env.NODE_ENV = 'production'
    
    // 复用loader
    const commonCssLoader = [
      // 提取成单独文件
      MiniCssExtractPlugin.loader,
      'css-loader',
      {
        loader: 'postcss-loader',
        options: {
          ident: 'postcss',
          plugins: () => [require('postcss-preset-env')()]
        }
    
      }
    ]
    
    module.exports = {
      entry: './src/js/index.js',
      output: {
        // 文件名
        filename: 'js/built.[contenthash:10].js',
        // 输出的文件目录
        path: resolve(__dirname, 'build')
      },
      module: {
        rules: [
          // 处理js文件
          {
            // 在package.json文件中eslintConfig配置使用airbnb规则
            test: /\.js$/,
            exclude: /node_modules/, // 排除检测node_modules的内容
            // 优先执行该loader,确保两个loader不会产生冲突
            enforce: 'pre',
            loader: 'eslint-loader',
            options: {
              // 自动修复检查到的问题
              fix: true
            }
          },
          {
            // 以下loader只会匹配一个
            // 注意:不能有两个配置处理同一种类型的文件
            oneOf: [
              // 处理css样式文件
              {
                test: /\.css$/,
                use: [...commonCssLoader]
              },
              // 处理less文件
              {
                test: /\.less$/,
                use: [
                  ...commonCssLoader,
                  // 将less文件编译成css文件
                  // 需要下载less-loader和less
                  'less-loader'
                ]
              },
              // 对js做兼容性处理
              {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                options: {
                  // 预设
                  presets: [
                    [
                      '@babel/preset-env', 
                      {
                        // 实现按需加载
                        useBuiltIns: 'usage',
                        // 指定core-js版本,可以到package.json查看
                        corejs: {
                          version: 3
                        },
                        // 指定兼容哪些版本的浏览器
                        targets: {
                          chrome: '60',
                          firefox: '50',
                          ie: '9'
                        }
                      }
                    ]
                  ],
                  // 开启babel缓存
                  // 第二次构建时,会读取之前的缓存
                  cacheDirectory: true
                }
              },
              // 处理图片文件
              {
                test: /\.(jpg|png|gif)/,
                loader: 'url-loader',
                options: {
                  // 图片大小小于8kb,就会被base64处理(8~12kb)
                  // 优点:减少请求数量(减轻服务器压力)
                  // 缺点:图片体积会更大(文件请求速度更慢)
                  limit: 8 * 1024,
                  // 给图片重命名
                  // [hash: 10]取图片的hash的前10位
                  // [ext]取文件原来扩展名
                  name: '[hash:10].[ext]',
                  // 输出路径
                  outputPath: 'imgs',
                  /**
                   * 问题:因为url-loader默认使用ES6模块化解析,而html-loader引入图片是commonjs
                   * 解析时会出问题:[object Module]
                   * 解决:关闭url-loader的ES6模块化,使用commonjs解析
                   */
                  esModule: false,
                }
              }, 
              {
                test: /\.html$/,
                // 处理html文件中的img图片(负责引入img,从而能被url-loader进行处理)
                loader: 'html-loader'
              },
              // 处理其它文件
              {
                // 排除以下几种文件
                exclude: /\.(js|css|less|html|jpg|png|gif)/,
                loader: 'file-loader',
                options: {
                  outputPath: 'media'
                }
              }
            ]
          }
        ]
      },
      plugins: [
        new MiniCssExtractPlugin({
          // 对输出的文件进行重命名
          filename: 'css/built.[contenthash:10].css'
        }),
        // 压缩css
        new OptimizeCssAssetsWebpackPlugin(),
        // 处理html文件
        new HtmlWebpackPlugin({
          // 以指定html文件为模板创建新的html文件
          template: './src/index.html',
          // 压缩html文件
          minify: {
            // 压缩空格
            collapseWhitespace: true,
            // 去除注释
            removeComments: true
          }
        }),
        new WorkboxWebpackPlugin.GenerateSW({
          /**
           * 1.帮助serviceworker快速启动
           * 2.删除就得serviceworker
           * 
           * 会生成一个serviceworker 配置文件
           */
          clientsClaim: true,
          skipWaiting: true
        })
      ],
      // 生产环境下js会自动压缩
      mode: 'production',
      devtool: 'source-map'
    }
    
  • 修改入口文件

    • 注册serviceworker
    • 处理兼容性问题
    import { mul } from './test';
    import '../css/index.css';
    
    function sum(...args) {
      return args.reduce((p, c) => p + c, 0);
    }
    
    console.log(mul(2, 3));
    
    // eslint-disable-next-line
    console.log(sum(1, 2, 3))
    
    // 注册serviceworker
    // 处理兼容性问题
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js')
          .then(() => {
            console.log('sw注册成功了');
          })
          .catch(() => {
            console.log('sw注册失败');
          });
      });
    }
    
    
  • 问题:eslint不认识window、navigator全局变量

  • 解决:需要修改package.json中eslintConfig配置

    "eslintConfig": {
        "extends": "airbnb-base",
        "env": {
          "browser": true
        }
      }
    
2.2.5.2.运行webpack

自动化构建工具——14.webpack 性能优化_第13张图片
可以看到构建目录下生成了一个service-worker.js文件,该文件通过注册的方式加载service-worker资源,通过该文件做一些相应的pwa工作。

2.2.5.3验证该技术是否应用成功

注:sw代码必须运行在服务器上,所以必须通过服务器的方式启动构建后的资源

启动服务器方式

  • nodejs

  • serve

    这里采用该方式来进行演示

    • npm i server -g
      
    • serve -s build: 启动服务器,将build目录下所有资源作为静态资源暴露出去
      

    自动化构建工具——14.webpack 性能优化_第14张图片

    • 查看是否应用成功
      自动化构建工具——14.webpack 性能优化_第15张图片
      自动化构建工具——14.webpack 性能优化_第16张图片
    • 将网络调整为offline再次刷新
      自动化构建工具——14.webpack 性能优化_第17张图片
      可以发现我们仍然可以对页面进行访问
      自动化构建工具——14.webpack 性能优化_第18张图片

你可能感兴趣的:(自动化构建工具——14.webpack 性能优化)