当前位置 博文首页 > 毛小星的博客:Webpack打包优化

    毛小星的博客:Webpack打包优化

    作者:[db:作者] 时间:2021-08-11 21:48

    Webpack打包优化


    Webapck 4 之后默认为我们做了很多配置项,内部开启了很多优化功能。对于开发人员,这种开箱即用的体验显然是很好的,但是同时也会导致我们忽略了很多需要学习的东西,一旦出现什么问题的时候,我们就无从下手了,下面我们就来看一下主要的优化配置项。

    DefinePlugin

    DefinePlugin 是用来为我们的代码来注入全局成员的,在 production 模式下,这个插件就会默认开启。它会在我们的环境中注入了一个 process.env.NODE_ENV 这样一个环境变量,我们可以通过这个环境变量去判断运行环境,从而去执行一些相应的逻辑。

    const webpack = require('webpack')
    
    module.exports = {
      mode: 'none',
      entry: './src/main.js',
      output: {
        filename: 'bundle.js'
      },
      plugins: [
        new webpack.DefinePlugin({
          // 值要求的是一个代码片段
          API_BASE_URL: JSON.stringify('https://api.example.com')
        })
      ]
    }
    

    这样我们就可以直接在环境中使用 API_BASE_URL 这个变量了

    // main.js
    console.log(API_BASE_URL)
    

    Tree-shaking

    Tree-shaking 顾名思义就是摇树,伴随着摇这个动作,我们会将树上的枯树枝和枯树叶摇下来。而在我们的项目中 Tree-shaking 会将我们代码中没有引用的部分去掉,Tree-shaking 并不是某一个配置选项,它是一组功能搭配使用的效果。我们可以使用 optimization 去开启一些功能,optimization 就是优化的意思,下面我们来看看怎样去配置它

    module.exports = {
      mode: 'none',
      entry: './src/index.js',
      output: {
        filename: 'bundle.js'
      },
      optimization: {
        // 模块只导出被使用的成员
        usedExports: true,
        // 尽可能合并每一个模块到一个函数中
        concatenateModules: true,
        // 压缩输出结果
        // minimize: true
      }
    }
    

    我们可以将 usedExports 想象成它就是去标记"枯树叶"的,而 minimize 就是去摇下这些枯树叶的。而 concatenateModules 将所有的代码都尽可能的合并到一个函数中去,这样既提升了运行效率,又减少了代码的体积。这个特性又被称为 Scope Hoisting,这时 Webpack3 中提出的一个特性。

    • Tree-shaking 与 babel

    由于 Webpack 的发展比较快,所以我们在找资料的时候,找到的资料并不一定适用于我们当前的版本,Tree-shaking 更是如此,很多资料中都显示如果我们使用的 babel-loader 的话,就会导致 Tree-shaking 失效。因为 Tree-shaking 使用的前提就是必须使用 ES Modules 规范去组织我们的代码,而 @babel/preset-env 这个插件内部就会将 ES Modules 的代码转换为 commonjs 代码的方式,所以 Tree-shaking 就不能生效。但是实际你同时开启两者的话,Tree-shaking 还是会生效的,因为 @babel/preset-env 这个插件最新的版本内部将 ES Modules 转换为 commonjs 关掉了。

    module.exports = {
      mode: 'none',
      entry: './src/index.js',
      output: {
        filename: 'bundle.js'
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: [
                  // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
                  // ['@babel/preset-env', { modules: 'commonjs' }]
                  // ['@babel/preset-env', { modules: false }]
                  // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
                  ['@babel/preset-env', { modules: 'auto' }]
                ]
              }
            }
          }
        ]
      },
      optimization: {
        // 模块只导出被使用的成员
        usedExports: true,
        // 尽可能合并每一个模块到一个函数中
        // concatenateModules: true,
        // 压缩输出结果
        // minimize: true
      }
    }
    

    sideEffects

    Webpack4 中还新增了一个叫 sideEffects 的新特性,它允许我们去标识我们的代码是否有副作用,从而为 Tree shaking 提供更大的压缩空间。副作用就是模块去执行时除了导出成员之外所做的事情,sideEffects 一般只有我们在去开发一个 npm 模块的时候才会去使用,那是因为官网将 sideEffects 和 Tree shaking 混到了一起,所以很多人误认为它们两个是因果关系,其实它们两个的关系不大。
    当我们去封装组件的时候,我们一般会将所有的组件都导入在一个文件中,然后通过这个文件集体导出,但是其他文件引入这个文件的时候,就会将这个导出文件的所有组件都引入

    // components/index.js
    export { default as Button } from './button'
    export { default as Heading } from './heading'
    
    // main.js
    import { Button } from './components'
    document.body.appendChild(Button())
    

    这样 Webpack 在打包的时候,也会将 Heading 组件打包到文件中,这时 sideEffects 就能解决这个问题

    module.exports = {
      mode: 'none',
      entry: './src/index.js',
      output: {
        filename: 'bundle.js'
      },
      optimization: {
        sideEffects: true,
      }
    }
    

    同时我们在 packag.json 中导入将没有副作用的文件关闭,这样就不会将无用的文件打包到项目中了

    {
      "name": "side-effects",
      "version": "0.1.0",
      "main": "index.js",
      "author": "maoxiaoxing",
      "license": "MIT",
      "scripts": {
        "build": "webpack"
      },
      "devDependencies": {
        "webpack": "^4.41.2",
        "webpack-cli": "^3.3.9"
      },
      "sideEffects": false
    }
    

    使用 sideEffects 的需要注意的是,我们的代码中真的没有副作用,如果有副作用的代码,我们就不能去这样配置了。

    // exten.js
    // 为 Number 的原型添加一个扩展方法
    Number.prototype.pad = function (size) {
      // 将数字转为字符串 => '8'
      let result = this + ''
      // 在数字前补指定个数的 0 => '008'
      while (result.length < size) {
        result = '0' + result
      }
      return result
    }
    

    例如我们在 extend.js 文件中为 Number 的原型添加一个方法,我们并没有向外导出成员,只是基于原型扩展了一个方法,我们在其他文件导入这个 extend.js

    // main.js
    // 副作用模块
    import './extend'
    console.log((8).pad(3))
    

    如果我们还标识项目中所有模块没有副作用的话,这个添加在原型的方法就不会被打包进去,在运行中肯定会报错,还有就是我们在代码中导入的 css 模块,也都是副作用模块,我们就可以在 package.json 中去标识我们的副作用模块

    {
      "name": "side-effects",
      "version": "0.1.0",
      "main": "index.js",
      "author": "maoxiaoxing",
      "license": "MIT",
      "scripts": {
        "build": "webpack"
      },
      "devDependencies": {
        "webpack": "^4.41.2",
        "webpack-cli": "^3.3.9"
      },
      "sideEffects": [
        "./src/extend.js",
        "*.css"
      ]
    }
    

    这样标识的有副作用的模块也会被打包进来。

    Webpack 代码分割(Code Splitting)

    模块化的优势固然很明显,但是也存在一些弊端,就是在我们的项目中所有的代码都会被打包到一起,如果我们的项目过大的话,那么我们的打包结果就会特别大。但是实际的情况是,我们在首次加载的时候,并不是所有的模块都是必须加载的,但是这些模块又被打包到一起,所以一方面在浏览器运行的时候会慢,一方面也会浪费一些流量和带宽。所以合理的方式就是将我们的代码按照一定的规则打包到多个 js 文件中去,做分包处理、按需加载,这样我们就会大大提高我们的应用的响应速率。那么有人可能会想到 Webpack 不就是将我们代码中散落的代码合并到一个函数中去执行,从而去提高效率,这里为什么又要做分包处理,不是自相矛盾吗?其实任何事情都是物极必反,Webpack 做代码合并是因为我们在开发中往往模块化颗粒度太细,所以 Webpack 必须将很多代码合并到一起,但是如果总体代码量过大的话,就会导致我们的单个打包文件过大,反而影响效率。所以模块化颗粒度太小不行,太大也不行,而 Code Splitting 就是为了解决我们模块化颗粒度太大的问题。

    • 多入口打包

    多入口打包就是将一个页面作为一个打包入口,而对于不同页面中公共的部分再去提取到公共的文件中去,而多入口打包的配置也很容易

    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
      mode: 'none',
      entry: { // 多入口打包,多个入口文件
        index: './src/index.js',
        album: './src/album.js'
      },
      output: {
        filename: '[name].bundle.js' // 由于多入口打包,采用占位符
      },
      optimization: {
        splitChunks: {
          // 自动提取所有公共模块到单独 bundle
          chunks: 'all'
        }
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/index.html',
          filename: 'index.html',
          chunks: ['index'] // 配置chunk,防止同时载入
        }),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/album.html',
          filename: 'album.html',
          chunks: ['album']
        })
      ]
    }
    
    • Webpack 按需加载

    按需加载是我们在开发中常见的需求,我们在处理打包的时候,我们可以需要哪个模块是,再加载哪个模块。Webpack 中支持动态导入的方式去支持按需加载我们的模块,所有动态加载的模块都会被自动分包,相比于分包加载的方式,动态加载的方式更加灵活。
    例如我们有两个模块 album 和 posts,我们就可以使用 import 去实现动态导入,import 返回一个 promise 对象,

    // ./posts/posts.js
    export default () => {
        const posts = document.createElement('div')
        posts.className = 'posts'
        ...
        return posts
    }
    
    // ./album/album.js
    export default () => {
        const album = document.createElement('div')
        album.className = 'album'
        ...
        return album
    }
    
    // import posts from './posts/posts'
    // import album from './album/album'
    
    const render = () => {
      const hash = window.location.hash || '#posts'
    
      const mainElement = document.querySelector('.main')
    
      mainElement.innerHTML = ''
    
      if (hash === '#posts') {
        // mainElement.appendChild(posts())\
        // 魔法注释:给模块重命名
        import(/* webpackChunkName: 'components' */'./posts/posts')
    
    下一篇:没有了