美文网首页
第四章:优化(深入浅出 Webpack 笔记)

第四章:优化(深入浅出 Webpack 笔记)

作者: 欢欣的膜笛 | 来源:发表于2020-12-04 23:12 被阅读0次

    Webpack 启动后会从配置的 Entry 出发,解析出文件中的导入语句,再递归的解析。

    缩小文件搜索范围

    • 优化 loader 配置
      为了尽可能少的让文件被 Loader 处理,可以通过 include 去命中只有哪些文件需要被处理。
    • 优化 resolve.modules 配置
      可以指明存放第三方模块的绝对路径,以减少寻找。
    • 优化 resolve.mainFields 配置
      用于配置采用哪个字段作为入口文件的描述。

    使用本方法优化时,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段,就算有一个模块搞错了都可能会造成构建出的代码无法正常运行。

    • 优化 resolve.alias 配置
      通过别名来把原导入路径映射成一个新的导入路径。
    • 优化 resolve.extensions 配置
      在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试询问文件是否存在,resolve.extensions 用于配置在尝试过程中用到的后缀列表。
    • 优化 module.noParse 配置
      Webpack 忽略对部分没采用模块化的文件的递归解析处理,提高构建性能。

    被忽略掉的文件里不应该包含 import 、 require 、 define 等模块化语句,不然会导致构建出的代码中包含无法在浏览器环境下执行的模块化语句。

    使用 DllPlugin(webpack4不建议使用)

    把网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中去。一个动态链接库中可以包含多个模块。
    Webpack 已经内置了对动态链接库的支持,需要通过2个内置的插件接入,它们分别是:

    • DllPlugin 插件:用于打包出一个个单独的动态链接库文件。
    • DllReferencePlugin 插件:用于在主要配置文件中去引入 DllPlugin 插件打包好的动态链接库文件。

    使用 HappyPack

    运行在 Node.js 之上的 Webpack 是单线程模型的,
    HappyPack 把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,从而让 Webpack 同一时刻处理多个任务,发挥多核 CPU 电脑的威力。

    由于 JavaScript 是单线程模型,要想发挥多核 CPU 的能力,只能通过多进程去实现,而无法通过多线程实现。

    使用 ParallelUglifyPlugin

    Webpack 有多个 JavaScript 文件需要输出和压缩时,会使用 UglifyJS 去一个个挨着压缩再输出, 但是 ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。 所以 ParallelUglifyPlugin 能更快的完成对多个文件的压缩工作。

    使用自动刷新

    Webpack 开启监听模式,有两种方式:

    • 在配置文件 webpack.config.js 中设置 watch: true
    • 在执行启动 Webpack 命令时,带上 --watch 参数,完整命令是 webpack --watch

    文件监听工作原理:
    定时获取文件的最后编辑时间,并存储,如果发现当前获取的和最后一次保存的最后编辑时间不一致,就认为该文件发生了变化。 watchOptions.poll 就是用于控制定时检查的周期,具体含义是每隔多少毫秒检查一次。
    当发现某个文件发生了变化时,并不会立刻告诉监听者,而是先缓存起来,收集一段时间的变化后,再一次性告诉监听者。 watchOptions.aggregateTimeout 就是用于配置这个等待时间。
    默认情况下 Webpack 会从配置的 Entry 文件出发,递归解析出 Entry 文件所依赖的文件,并都加入到监听列表中去。
    由于保存文件的路径和最后编辑时间需要占用内存,定时检查周期检查需要占用 CPU 以及文件 I/O,所以最好减少需要监听的文件数量和降低检查频率。

    优化文件监听性能:ignored: /node_modules/

    自动刷新浏览器:
    webpack 模块负责监听文件,webpack-dev-server 模块则负责刷新浏览器。 在使用 webpack-dev-server 模块去启动 webpack 模块时,webpack 模块的监听模式默认会被开启。 webpack 模块会在文件发生变化时告诉 webpack-dev-server 模块。

    自动刷新的原理:

    1. 借助浏览器扩展去通过浏览器提供的接口刷新,WebStorm IDE 的 LiveEdit 功能就是这样实现的。
    2. 往要开发的网页中注入代理客户端代码,通过代理客户端去刷新整个页面。
    3. 把要开发的网页装进一个 iframe 中,通过刷新 iframe 去看到最新效果。
      DevServer 支持第2、3种方法,第2种是 DevServer 默认采用的刷新方法。

    开启模块热替换

    当一个源码发生变化时,只重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块。
    DevServer 默认不会开启模块热替换模式,要开启该模式,只需在启动时带上参数 --hot,完整命令是 webpack-dev-server --hot

    区分环境

    当你的代码中出现了使用 process 模块的语句时,Webpack 就自动打包进 process 模块的代码以支持非 Node.js 的运行环境。 这个注入的 process 模块作用是为了模拟 Node.js 中的 process,以支持上面使用的 process.env.NODE_ENV === 'production' 语句。

    const DefinePlugin = require('webpack/lib/DefinePlugin');
    module.exports = {
      plugins: [
        new DefinePlugin({
          // 定义 NODE_ENV 环境变量为 production
          'process.env': {
            NODE_ENV: JSON.stringify('production')
          }
        }),
      ],
    };
    

    注意在定义环境变量的值时用 JSON.stringify 包裹字符串的原因是环境变量的值需要是一个由双引号包裹的字符串,而 JSON.stringify('production')的值正好等于'"production"'

    压缩代码

    • 压缩 JavaScript
      目前最成熟的 JavaScript 代码压缩工具是 UglifyJS , 它会分析 JavaScript 代码语法树,理解代码含义,从而能做到诸如去掉无效代码、去掉日志输出代码、缩短变量名等优化。
      直接在启动 Webpack 时带上 --optimize-minimize 参数,即 webpack --optimize-minimize, 这样 Webpack 会自动为你注入一个带有默认配置的 UglifyJSPlugin

    • 压缩 ES6
      UglifyJS 只认识 ES5 语法的代码。 为了压缩 ES6 代码,需要使用专门针对 ES6 代码的 UglifyES

    • 压缩 CSS
      要开启 cssnano 去压缩代码只需要开启 css-loaderminimize 选项。

    CDN 加速

    CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。
    CDN 其实是通过优化物理链路层传输过程中的网速有限、丢包等问题来加速网络传输的。
    publicPath 参数设置存放静态资源的 CDN 目录 URL, 为了让不同类型的资源输出到不同的 CDN,需要分别在:

    • output.publicPath 中设置 JavaScript 的地址。
    • css-loader.publicPath 中设置被 CSS 导入的资源的的地址。
    • WebPlugin.stylePublicPath 中设置 CSS 文件的地址。

    使用 Tree Shaking

    Tree Shaking 可以用来剔除 JavaScript中用不上的死代码。它依赖静态的 ES6 模块化语法。

    提取公共代码

    Webpack 内置了专门用于提取多个 Chunk 中公共部分的插件 CommonsChunkPlugin

    按需加载

    使用 Prepack

    Prepack 在保持运行结果一致的情况下,改变源代码的运行逻辑,输出性能更高的 JavaScript 代码。 实际上 Prepack 就是一个部分求值器,编译代码时提前将计算结果放到编译后的代码中,而不是在代码运行时才去求值,从而优化代码在运行时的效率。

    Prepack 还处于初期的开发阶段,局限性也很大。

    开启 Scope Hoisting,又叫作用域提升

    原理:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。 因此只有那些被引用了一次的模块才能被合并。

    输出分析

    接入 webpack-bundle-analyzer

    • npm i -g webpack-bundle-analyzer
    • 启动 Webpack 命令: webpack --profile --json > stats.json
    • 在项目根目录中执行 webpack-bundle-analyzer 后,浏览器会打开对应网页看到分析结果。

    又话总结

    • 侧重优化开发体验的配置文件 webpack.config.js
    const path = require('path');
    const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
    const {AutoWebPlugin} = require('web-webpack-plugin');
    const HappyPack = require('happypack');
    
    // 自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用
    const autoWebPlugin = new AutoWebPlugin('./src/pages', {
      // HTML 模版文件所在的文件路径
      template: './template.html',
      // 提取出所有页面公共的代码
      commonsChunk: {
        // 提取出公共代码 Chunk 的名称
        name: 'common',
      },
    });
    
    module.exports = {
      // AutoWebPlugin 会为寻找到的所有单页应用,生成对应的入口配置,
      // autoWebPlugin.entry 方法可以获取到生成入口配置
      entry: autoWebPlugin.entry({
        // 这里可以加入你额外需要的 Chunk 入口
        base: './src/base.js',
      }),
      output: {
        filename: '[name].js',
      },
      resolve: {
        // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
        // 其中 __dirname 表示当前工作目录,也就是项目根目录
        modules: [path.resolve(__dirname, 'node_modules')],
        // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件,使用 Tree Shaking 优化
        // 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
        mainFields: ['jsnext:main', 'main'],
      },
      module: {
        rules: [
          {
            // 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
            test: /\.js$/,
            // 使用 HappyPack 加速构建
            use: ['happypack/loader?id=babel'],
            // 只对项目根目录下的 src 目录中的文件采用 babel-loader
            include: path.resolve(__dirname, 'src'),
          },
          {
            test: /\.js$/,
            use: ['happypack/loader?id=ui-component'],
            include: path.resolve(__dirname, 'src'),
          },
          {
            // 增加对 CSS 文件的支持
            test: /\.css$/,
            use: ['happypack/loader?id=css'],
          },
        ]
      },
      plugins: [
        autoWebPlugin,
        // 使用 HappyPack 加速构建
        new HappyPack({
          id: 'babel',
          // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
          loaders: ['babel-loader?cacheDirectory'],
        }),
        new HappyPack({
          // UI 组件加载拆分
          id: 'ui-component',
          loaders: [{
            loader: 'ui-component-loader',
            options: {
              lib: 'antd',
              style: 'style/index.css',
              camel2: '-'
            }
          }],
        }),
        new HappyPack({
          id: 'css',
          // 如何处理 .css 文件,用法和 Loader 配置中一样
          loaders: ['style-loader', 'css-loader'],
        }),
        // 提取公共代码
        new CommonsChunkPlugin({
          // 从 common 和 base 两个现成的 Chunk 中提取公共的部分
          chunks: ['common', 'base'],
          // 把公共的部分放到 base 中
          name: 'base'
        }),
      ],
      watchOptions: {
        // 使用自动刷新:不监听的 node_modules 目录下的文件
        ignored: /node_modules/,
      }
    };
    
    • 侧重优化输出质量的配置文件webpack-dist.config.js
    const path = require('path');
    const DefinePlugin = require('webpack/lib/DefinePlugin');
    const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
    const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    const {AutoWebPlugin} = require('web-webpack-plugin');
    const HappyPack = require('happypack');
    const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
    
    // 自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用
    const autoWebPlugin = new AutoWebPlugin('./src/pages', {
      // HTML 模版文件所在的文件路径
      template: './template.html',
      // 提取出所有页面公共的代码
      commonsChunk: {
        // 提取出公共代码 Chunk 的名称
        name: 'common',
      },
      // 指定存放 CSS 文件的 CDN 目录 URL
      stylePublicPath: '//css.cdn.com/id/',
    });
    
    module.exports = {
      // AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置,
      // autoWebPlugin.entry 方法可以获取到生成入口配置
      entry: autoWebPlugin.entry({
        // 这里可以加入你额外需要的 Chunk 入口
        base: './src/base.js',
      }),
      output: {
        // 给输出的文件名称加上 Hash 值
        filename: '[name]_[chunkhash:8].js',
        path: path.resolve(__dirname, './dist'),
        // 指定存放 JavaScript 文件的 CDN 目录 URL
        publicPath: '//js.cdn.com/id/',
      },
      resolve: {
        // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
        // 其中 __dirname 表示当前工作目录,也就是项目根目录
        modules: [path.resolve(__dirname, 'node_modules')],
        // 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
        mainFields: ['jsnext:main', 'main'],
      },
      module: {
        rules: [
          {
            // 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
            test: /\.js$/,
            // 使用 HappyPack 加速构建
            use: ['happypack/loader?id=babel'],
            // 只对项目根目录下的 src 目录中的文件采用 babel-loader
            include: path.resolve(__dirname, 'src'),
          },
          {
            test: /\.js$/,
            use: ['happypack/loader?id=ui-component'],
            include: path.resolve(__dirname, 'src'),
          },
          {
            // 增加对 CSS 文件的支持
            test: /\.css$/,
            // 提取出 Chunk 中的 CSS 代码到单独的文件中
            use: ExtractTextPlugin.extract({
              use: ['happypack/loader?id=css'],
              // 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
              publicPath: '//img.cdn.com/id/'
            }),
          },
        ]
      },
      plugins: [
        autoWebPlugin,
        // 开启ScopeHoisting
        new ModuleConcatenationPlugin(),
        // 使用HappyPack
        new HappyPack({
          // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
          id: 'babel',
          // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
          loaders: ['babel-loader?cacheDirectory'],
        }),
        new HappyPack({
          // UI 组件加载拆分
          id: 'ui-component',
          loaders: [{
            loader: 'ui-component-loader',
            options: {
              lib: 'antd',
              style: 'style/index.css',
              camel2: '-'
            }
          }],
        }),
        new HappyPack({
          id: 'css',
          // 如何处理 .css 文件,用法和 Loader 配置中一样
          // 通过 minimize 选项压缩 CSS 代码
          loaders: ['css-loader?minimize'],
        }),
        new ExtractTextPlugin({
          // 给输出的 CSS 文件名称加上 Hash 值
          filename: `[name]_[contenthash:8].css`,
        }),
        // 提取公共代码
        new CommonsChunkPlugin({
          // 从 common 和 base 两个现成的 Chunk 中提取公共的部分
          chunks: ['common', 'base'],
          // 把公共的部分放到 base 中
          name: 'base'
        }),
        new DefinePlugin({
          // 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才需要的部分
          'process.env': {
            NODE_ENV: JSON.stringify('production')
          }
        }),
        // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
        new ParallelUglifyPlugin({
          // 传递给 UglifyJS 的参数
          uglifyJS: {
            output: {
              // 最紧凑的输出
              beautify: false,
              // 删除所有的注释
              comments: false,
            },
            compress: {
              // 在UglifyJs删除没有用到的代码时不输出警告
              warnings: false,
              // 删除所有的 `console` 语句,可以兼容ie浏览器
              drop_console: true,
              // 内嵌定义了但是只用到一次的变量
              collapse_vars: true,
              // 提取出出现多次但是没有定义成变量去引用的静态值
              reduce_vars: true,
            }
          },
        }),
      ]
    };
    

    相关文章

      网友评论

          本文标题:第四章:优化(深入浅出 Webpack 笔记)

          本文链接:https://www.haomeiwen.com/subject/lazmwktx.html