zoukankan      html  css  js  c++  java
  • webpack优化

    webpack优化

    production模式打包自带优化

    • tree shaking

      tree shaking 是一个术语,通常用于打包时移除 JavaScript 中的未引用的代码(dead-code),它依赖于 ES6 模块系统中 importexport静态结构特性。

      开发时引入一个模块后,如果只使用其中一个功能,上线打包时只会把用到的功能打包进bundle,其他没用到的功能都不会打包进来,可以实现最基础的优化

    • scope hoisting

      scope hoisting的作用是将模块之间的关系进行结果推测, 可以让 Webpack 打包出来的代码文件更小、运行的更快

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

      由于 scope hoisting 需要分析出模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效。
      原因和tree shaking一样。

    • 代码压缩

      所有代码使用UglifyJsPlugin插件进行压缩、混淆

    css优化

    将css提取到独立的文件中

    mini-css-extract-plugin是用于将CSS提取为独立的文件的插件,对每个包含css的js文件都会创建一个CSS文件,支持按需加载css和sourceMap

    只能用在webpack4中,有如下优势:

    • 异步加载
    • 不重复编译,性能很好
    • 容易使用
    • 只针对CSS

    使用方法:

    1. 安装

      npm i -D mini-css-extract-plugin

    2. 在webpack配置文件中引入插件

      const MiniCssExtractPlugin = require('mini-css-extract-plugin')
      
    3. 创建插件对象,配置抽离的css文件名,支持placeholder语法

      new MiniCssExtractPlugin({
      	filename: '[name].css'
      })
      
    4. 将原来配置的所有style-loader替换为MiniCssExtractPlugin.loader

      {
      test: /.css$/,
      // webpack读取loader时 是从右到左的读取, 会将css文件先交给最右侧的loader来处理
      

    // loader的执行顺序是从右到左以管道的方式链式调用
    // css-loader: 解析css文件
    // style-loader: 将解析出来的结果 放到html中, 使其生效
    // use: ['style-loader', 'css-loader']
    use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
    },
    // { test: /.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
    { test: /.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] },
    // { test: /.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
    { test: /.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] },

    
    
    ### 自动添加css前缀
    
    使用`postcss`,需要用到`postcss-loader`和`autoprefixer`插件
    
    1. 安装
    
    `npm i -D postcss-loader autoprefixer`
    
    2. 修改webpack配置文件中的loader,将`postcss-loader`放置在`css-loader`的右边(调用链从右到左)
    
    ```js
    {
    test: /.css$/,
    // webpack读取loader时 是从右到左的读取, 会将css文件先交给最右侧的loader来处理
    // loader的执行顺序是从右到左以管道的方式链式调用
    // css-loader: 解析css文件
    // style-loader: 将解析出来的结果 放到html中, 使其生效
    // use: ['style-loader', 'css-loader']
    use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
    },
    // { test: /.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
    { test: /.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] },
    // { test: /.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
    { test: /.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] },
    
    1. 项目根目录下添加postcss的配置文件:postcss.config.js

    2. postcss的配置文件中使用插件

      module.exports = {
        plugins: [require('autoprefixer')]
      }
      

    开启css压缩

    需要使用optimize-css-assets-webpack-plugin插件来完成css压缩

    但是由于配置css压缩时会覆盖掉webpack默认的优化配置,导致JS代码无法压缩,所以还需要手动把JS代码压缩插件导入进来:terser-webpack-plugin

    1. 安装

      npm i -D optimize-css-assets-webpack-plugin terser-webpack-plugin

    2. 导入插件

      const TerserJSPlugin = require('terser-webpack-plugin')
      const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
      
    3. 在webpack配置文件中添加配置节点

      optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
      },
      

    tips: webpack4默认采用的JS压缩插件为:uglifyjs-webpack-plugin,在mini-css-extract-plugin上一个版本中还推荐使用该插件,但最新的v0.6中建议使用teser-webpack-plugin来完成js代码压缩,具体原因未在官网说明,我们就按照最新版的官方文档来做即可

    js代码分离

    Code Splitting是webpack打包时用到的重要的优化特性之一,此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

    有三种常用的代码分离方法:

    • 入口起点(entry points):使用entry配置手动地分离代码。
    • 防止重复(prevent duplication):使用 SplitChunksPlugin去重和分离 chunk。
    • 动态导入(dynamic imports):通过模块的内联函数调用来分离代码。

    手动配置多入口

    1. 在webpack配置文件中配置多个入口

      entry: {
        main: './src/main.js',
        other: './src/other.js'
      },
      output: {
        // path.resolve() : 解析当前相对路径的绝对路径
        // path: path.resolve('./dist/'),
        // path: path.resolve(__dirname, './dist/'),
        path: path.join(__dirname, '..', './dist/'),
        // filename: 'bundle.js',
        filename: '[name].bundle.js',
        publicPath: '/'
      },
      
    2. 在main.js和other.js中都引入同一个模块,并使用其功能

      main.js

      import $ from 'jquery'
      
      $(function() {
        $('<div></div>').html('main').appendTo('body')
      })
      

      other.js

      import $ from 'jquery'
      
      $(function() {
        $('<div></div>').html('other').appendTo('body')
      })
      
    3. 修改package.json的脚本,添加一个使用dev配置文件进行打包的脚本(目的是不压缩代码检查打包的bundle时更方便)

      "scripts": {
          "build": "webpack --config ./build/webpack.prod.js",
          "dev-build": "webpack --config ./build/webpack.dev.js"
      }
      
    4. 运行npm run dev-build,进行打包

    5. 查看打包后的结果,发现other.bundle.js和main.bundle.js都同时打包了jQuery源文件

    这种方法存在一些问题:

    • 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
    • 这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。

    抽取公共代码

    tips: Webpack v4以上使用的插件为SplitChunksPlugin,以前使用的CommonsChunkPlugin已经被移除了,最新版的webpack只需要在配置文件中的optimization节点下添加一个splitChunks属性即可进行相关配置

    1. 修改webpack配置文件

      optimization: {
          splitChunks: {
            chunks: 'all'
          }
      },
      
    2. 运行npm run dev-build重新打包

    3. 查看dist目录

    1. 查看vendors~main~other.bundle.js,其实就是把都用到的jQuery打包到了一个单独的js中

    动态导入 (懒加载)

    webpack4默认是允许import语法动态导入的,但是需要babel的插件支持,最新版babel的插件包为:@babel/plugin-syntax-dynamic-import,以前老版本不是@babel开头,已经无法使用,需要注意

    动态导入最大的好处是实现了懒加载,用到哪个模块才会加载哪个模块,可以提高SPA应用程序的首屏加载速度,Vue、React、Angular框架的路由懒加载原理一样

    1. 安装babel插件

      npm install -D @babel/plugin-syntax-dynamic-import

    2. 修改.babelrc配置文件,添加@babel/plugin-syntax-dynamic-import插件

      {
        "presets": ["@babel/env"],
        "plugins": [
          "@babel/plugin-proposal-class-properties",
          "@babel/plugin-transform-runtime",
          "@babel/plugin-syntax-dynamic-import"
        ]
      }
      
    3. 将jQuery模块进行动态导入

      function getComponent() {
        return import('jquery').then(({ default: $ }) => {
          return $('<div></div>').html('main')
        })
      }
      
    4. 给某个按钮添加点击事件,点击后调用getComponent函数创建元素并添加到页面

      window.onload = function () {
        document.getElementById('btn').onclick = function () {
          getComponent().then(item => {
            item.appendTo('body')
          })
        }
      }
      

    SplitChunksPlugin配置参数

    webpack4之后,使用SplitChunksPlugin插件替代了以前CommonsChunkPlugin

    SplitChunksPlugin的配置,只需要在webpack配置文件中的optimization节点下的splitChunks进行修改即可,如果没有任何修改,则会使用默认配置

    默认的SplitChunksPlugin 配置适用于绝大多数用户

    webpack 会基于如下默认原则自动分割代码:

    • 公用代码块或来自 node_modules 文件夹的组件模块。
    • 打包的代码块大小超过 30k(最小化压缩之前)。
    • 按需加载代码块时,同时发送的请求最大数量不应该超过 5。
    • 页面初始化时,同时发送的请求最大数量不应该超过 3。

    以下是SplitChunksPlugin的默认配置:

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          chunks: 'async', // 只对异步加载的模块进行拆分,可选值还有all | initial
          minSize: 30000, // 模块最少大于30KB才拆分
          maxSize: 0,  // 模块大小无上限,只要大于30KB都拆分
          minChunks: 1, // 模块最少引用一次才会被拆分
          maxAsyncRequests: 5, // 异步加载时同时发送的请求数量最大不能超过5,超过5的部分不拆分
          maxInitialRequests: 3, // 页面初始化时同时发送的请求数量最大不能超过3,超过3的部分不拆分
          automaticNameDelimiter: '~', // 默认的连接符
          name: true, // 拆分的chunk名,设为true表示根据模块名和CacheGroup的key来自动生成,使用上面连接符连接
          cacheGroups: { // 缓存组配置,上面配置读取完成后进行拆分,如果需要把多个模块拆分到一个文件,就需要缓存,所以命名为缓存组
            vendors: { // 自定义缓存组名
              test: /[\/]node_modules[\/]/, // 检查node_modules目录,只要模块在该目录下就使用上面配置拆分到这个组
              priority: -10 // 权重-10,决定了哪个组优先匹配,例如node_modules下有个模块要拆分,同时满足vendors和default组,此时就会分到vendors组,因为-10 > -20
            },
            default: { // 默认缓存组名
              minChunks: 2, // 最少引用两次才会被拆分
              priority: -20, // 权重-20
              reuseExistingChunk: true // 如果主入口中引入了两个模块,其中一个正好也引用了后一个,就会直接复用,无需引用两次
            }
          }
        }
      }
    };
    

    noParse

    在引入一些第三方模块时,例如jQuery、bootstrap等,我们知道其内部肯定不会依赖其他模块,因为最终我们用到的只是一个单独的js文件或css文件

    所以此时如果webpack再去解析他们的内部依赖关系,其实是非常浪费时间的,我们需要阻止webpack浪费精力去解析这些明知道没有依赖的库

    可以在webpack配置文件的module节点下加上noParse,并配置正则来确定不需要解析依赖关系的模块

    module: {
    	noParse: /jquery|bootstrap/
    }
    

    IgnorePlugin

    在引入一些第三方模块时,例如moment,内部会做i18n国际化处理,所以会包含很多语言包,而语言包打包时会比较占用空间,如果我们项目只需要用到中文,或者少数语言,可以忽略掉所有的语言包,然后按需引入语言包

    从而使得构建效率更高,打包生成的文件更小

    需要忽略第三方模块内部依赖的其他模块,只需要三步:

    1. 首先要找到moment依赖的语言包是什么
    2. 使用IgnorePlugin插件忽略其依赖
    3. 需要使用某些依赖时自行手动引入

    具体实现如下:

    1. 通过查看moment的源码来分析:

      function loadLocale(name) {
          var oldLocale = null;
          // TODO: Find a better way to register and load all the locales in Node
          if (!locales[name] && (typeof module !== 'undefined') &&
              module && module.exports) {
              try {
                  oldLocale = globalLocale._abbr;
                  var aliasedRequire = require;
                  aliasedRequire('./locale/' + name);
                  getSetGlobalLocale(oldLocale);
              } catch (e) {}
          }
          return locales[name];
      }
      
      

      观察上方代码,同时查看moment目录下确实有locale目录,其中放着所有国家的语言包,可以分析得出:locale目录就是moment所依赖的语言包目录

    2. 使用IgnorePlugin插件来忽略掉moment模块的locale目录

      在webpack配置文件中安装插件,并传入配置项

      参数1:表示要忽略的资源路径

      参数2:要忽略的资源上下文(所在哪个目录)

      两个参数都是正则对象

      new webpack.IgnorePlugin(/./locale/, /moment/)
      
    3. 使用moment时需要手动引入语言包,否则默认使用英文

      import moment from 'moment'
      import 'moment/locale/zh-cn'
      moment.locale('zh-CN')
      console.log(moment().subtract(6, 'days').calendar())
      

    DllPlugin

    在引入一些第三方模块时,例如vue、react、angular等框架,这些框架的文件一般都是不会修改的,而每次打包都需要去解析它们,也会影响打包速度,哪怕做拆分,也只是提高了上线后用户访问速度,并不会提高构建速度,所以如果需要提高构建速度,应该使用动态链接库的方式,类似于Windows中的dll文件。

    借助DllPlugin插件实现将这些框架作为一个个的动态链接库,只构建一次,以后每次构建都只生成自己的业务代码,可以大大提高构建效率!

    主要思想在于,将一些不做修改的依赖文件,提前打包,这样我们开发代码发布的时候就不需要再对这部分代码进行打包,从而节省了打包时间。

    涉及两个插件:

    1. DllPlugin

      使用一个单独webpack配置创建一个dll文件。并且它还创建一个manifest.json。DllReferencePlugin使用该json文件来做映射依赖性。(这个文件会告诉我们的哪些文件已经提取打包好了)

      配置参数:

      • context (可选): manifest文件中请求的上下文,默认为该webpack文件上下文。
      • name: 公开的dll函数的名称,和output.library保持一致即可。
      • path: manifest.json生成的文件夹及名字
    2. DllReferencePlugin

      这个插件用于主webpack配置,它引用的dll需要预先构建的依赖关系。

      • context: manifest文件中请求的上下文。

      • manifest: DllPlugin插件生成的manifest.json

      • content(可选): 请求的映射模块id(默认为manifest.content)

      • name(可选): dll暴露的名称

      • scope(可选): 前缀用于访问dll的内容

      • sourceType(可选): dll是如何暴露(libraryTarget)

    将Vue项目中的库抽取成Dll

    1. 准备一份将Vue打包成DLL的webpack配置文件

      在build目录下新建一个文件:webpack.vue.js

      配置入口:将多个要做成dll的库全放进来

      配置出口:一定要设置library属性,将打包好的结果暴露在全局

      配置plugin:设置打包后dll文件名和manifest文件所在地

      const path = require('path')
      const webpack = require('webpack')
      module.exports = {
        mode: 'development',
        entry: {
          vue: [
            'vue/dist/vue.js',
            'vue-router'
          ]
        },
        output: {
          filename: '[name]_dll.js',
          path: path.resolve(__dirname, '../dist'),
          library: '[name]_dll'
        },
        plugins: [
          new webpack.DllPlugin({
            name: '[name]_dll',
            path: path.resolve(__dirname, '../dist/manifest.json')
          })
        ]
      }
      
    2. 在webpack.base.js中进行插件的配置

      使用DLLReferencePlugin指定manifest文件的位置即可

      new webpack.DllReferencePlugin({
        manifest: path.resolve(__dirname, '../dist/manifest.json')
      })
      
    3. 安装add-asset-html-webpack-plugin

      npm i add-asset-html-webpack-plugin -D

    4. 配置插件自动添加script标签到HTML中

      new AddAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, '../dist/vue_dll.js')
      })
      

    将React项目中的库抽取成Dll

    1. 准备一份将React打包成DLL的webpack配置文件

      在build目录下新建一个文件:webpack.vue.js

      配置入口:将多个要做成dll的库全放进来

      配置出口:一定要设置library属性,将打包好的结果暴露在全局

      配置plugin:设置打包后dll文件名和manifest文件所在地

      const path = require('path')
      const webpack = require('webpack')
      module.exports = {
        mode: 'development',
        entry: {
          react: [
            'react',
            'react-dom'
          ]
        },
        output: {
          filename: '[name]_dll.js',
          path: path.resolve(__dirname, '../dist'),
          library: '[name]_dll'
        },
        plugins: [
          new webpack.DllPlugin({
            name: '[name]_dll',
            path: path.resolve(__dirname, '../dist/manifest.json')
          })
        ]
      }
      
    2. 在webpack.base.js中进行插件的配置

      使用DLLReferencePlugin指定manifest文件的位置即可

      new webpack.DllReferencePlugin({
        manifest: path.resolve(__dirname, '../dist/manifest.json')
      })
      
    3. 安装add-asset-html-webpack-plugin

      npm i add-asset-html-webpack-plugin -D

    4. 配置插件自动添加script标签到HTML中

      new AddAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, '../dist/react_dll.js')
      })
      

    Happypack

    由于webpack在node环境中运行打包构建,所以是单线程的模式,在打包众多资源时效率会比较低下,早期可以通过Happypack来实现多进程打包。当然,这个问题只出现在低版本的webpack中,现在的webpack性能已经非常强劲了,所以无需使用Happypack也可以实现高性能打包

    Happypack官网

    引用官网原文:

    Maintenance mode notice

    My interest in the project is fading away mainly because I'm not using JavaScript as much as I was in the past. Additionally, Webpack's native performance is improving and (I hope) it will soon make this plugin unnecessary.

    See the FAQ entry about Webpack 4 and thread-loader.

    Contributions are always welcome. Changes I make from this point will be restricted to bug-fixing. If someone wants to take over, feel free to get in touch.

    Thanks to everyone who used the library, contributed to it and helped in refining it!!!

    由此可以看出作者已经发现,webpack的性能已经强大到不需要使用该插件了,而且小项目使用该插件反而会导致性能损耗过大,因为开启进程是需要耗时的

    使用方法:

    1. 安装插件

      npm i -D happypack

    2. 在webpack配置文件中引入插件

      const HappyPack = require('happypack')
      
    3. 修改loader的配置规则

      {
        test: /.js$/,
        use: {
            loader: 'happypack/loader'
          },
        include: path.resolve(__dirname, '../src'),
        exclude: /node_modules/
      }
      
    4. 配置插件

      new HappyPack({
          loaders: [ 'babel-loader' ]
      })
      
    5. 运行打包命令

      npm run build

    浏览器缓存

    在做了众多代码分离的优化后,其目的是为了利用浏览器缓存,达到提高访问速度的效果,所以构建项目时做代码分割是必须的,例如将固定的第三方模块抽离,下次修改了业务代码,重新发布上线不重启服务器,用户再次访问服务器就不需要再次加载第三方模块了

    但此时会遇到一个新的问题,如果再次打包上线不重启服务器,客户端会把以前的业务代码和第三方模块同时缓存,再次访问时依旧会访问缓存中的业务代码,所以会导致业务代码也无法更新

    需要在output节点的filename中使用placeholder语法,根据代码内容生成文件名的hash:

    output: {
        // path.resolve() : 解析当前相对路径的绝对路径
        // path: path.resolve('./dist/'),
        // path: path.resolve(__dirname, './dist/'),
        path: path.join(__dirname, '..', './dist/'),
        // filename: 'bundle.js',
        filename: '[name].[contenthash:8].bundle.js',
        publicPath: '/'
      },
    

    之后每次打包业务代码时,如果有改变,会生成新的hash作为文件名,浏览器就不会使用缓存了,而第三方模块不会重新打包生成新的名字,则会继续使用缓存

    打包分析

    项目构建完成后,需要通过一些工具对打包后的bundle进行分析,通过分析才能总结出一些经验,官方推荐的分析方法有两步完成:

    1. 使用--profile --json参数,以json格式来输出打包后的结果到某个指定文件中

      webpack --profile --json > stats.json

    2. 将stats.json文件放入工具中进行分析

      官方工具:official analyze tool

      官方推荐的其他四个工具:

      其中webpack-bundle-analyzer是一个插件,可以以插件的方式安装到项目中

    Prefetching和Preloading

    在优化访问性能时,除了充分利用浏览器缓存之外,还需要涉及一个性能指标:coverage rate(覆盖率)

    可以在Chrome浏览器的控制台中按:ctrl + shift + p,查找coverage,打开覆盖率面板

    开始录制后刷新网页,即可看到每个js文件的覆盖率,以及总的覆盖率

    想提高覆盖率,需要尽可能多的使用动态导入,也就是懒加载功能,将一切能使用懒加载的地方都是用懒加载,这样可以大大提高覆盖率

    但有时候使用懒加载会影响用户体验,所以可以在懒加载时使用魔法注释:Prefetching,是指在首页资源加载完毕后,空闲时间时,将动态导入的资源加载进来,这样即可以提高首屏加载速度,也可以解决懒加载可能会影响用户体验的问题,一举两得!

    function getComponent() {
      return import(/* webpackPrefetch: true */ 'jquery').then(({ default: $ }) => {
        return $('<div></div>').html('我是main')
      })
    }
    
  • 相关阅读:
    3.K均值算法
    2.机器学习相关数学基础
    机器算法第一次作业
    语法制导的语义翻译
    算符优先分析
    自下而上语法分析
    LL(1)文法的判断,递归下降分析程序
    消除左递归
    DFA最小化,语法分析初步
    词法分析程序的设计与实现
  • 原文地址:https://www.cnblogs.com/xm0328/p/14209977.html
Copyright © 2011-2022 走看看