Webpack版本升级与打包优化

1. 9.0版本

1.1 动态路由修改

  1. 打包分析
Show chunks: All (3.3 MB)
js/app.f6e590defd09f0af0533.js (1.23 MB)
js/0.dcb7a6fef441f8b29aad.js (759.71 KB)
js/1.12b87a568748b35be091.js (753 KB)
js/vendor.3a71817ce04ee0d52abd.js (477.57 KB)
js/2.b41119393c5fed1db5a7.js (50.59 KB)
js/3.1e900085df4e92b118d0.js (44.05 KB)
js/4.cd0ae5676c3c19029aaa.js (30.54 KB)
js/5.43b81f7e8267bd84e89e.js (2.27 KB)
js/6.8d54cf0b69937a8c4ca4.js (1.73 KB)
js/manifest.aab2c6e6e57bd770452c.js (1.58 KB)
image.png

(1) 动态路由生成js文件没有自定义名
(2) 0.js1.js代码几乎都是重复的。
(3) 2.js3.js4.js动态路由生成的代码体积太小。

  1. 设置打包配置chunkFilename
    webpack.config.production.js
    output: {
        path: config.path.dist,
-        filename: 'js/[name].[chunkhash].js',
-        chunkFilename: 'js/[id].[chunkhash].js',
+        filename: 'js/[name].[chunkhash:5].js',
+        chunkFilename: 'js/[name].[chunkhash:5].js',
        publicPath: '?op=resource&encode=utf8&resource=/com/fr/wei/plugin/h5reportnew/dist/'
    },
  1. 自定义chunkName并进行路由合并
    routerConfig.web.js
-import Login from 'bundle-loader?lazy!../../platform/view/Login/'
-import Directory from 'bundle-loader?lazy!../../platform/view/Directory'
-import ReportPage from 'bundle-loader?lazy!../../fr/page/ReportPage'
-import FormPage from 'bundle-loader?lazy!../../fr/page/FormPage'
-import WebPage from 'bundle-loader?lazy!../../platform/view/WebPage'
-import ChangePassword from 'bundle-loader?lazy!../../platform/view/ChangePassword/ChangePassword'
-import CprPage from 'bundle-loader?lazy!../../fr/page/CprPage'
+import Login from 'bundle-loader?lazy&name=platform!../../platform/view/Login/'
+import Directory from 'bundle-loader?lazy&name=platform!../../platform/view/Directory'
+import ReportPage from 'bundle-loader?lazy&name=FRPage!../../fr/page/ReportPage'
+import FormPage from 'bundle-loader?lazy&name=FRPage!../../fr/page/FormPage'
+import WebPage from 'bundle-loader?lazy&name=platform!../../platform/view/WebPage'
+import ChangePassword from 'bundle-loader?lazy&name=platform!../../platform/view/ChangePassword/ChangePassword'
+import CprPage from 'bundle-loader?lazy&name=platform!../../fr/page/CprPage'
  1. 打包分析
Show chunks: All (2.57 MB)
js/app.3027a.js (1.23 MB)
js/FRPage.792bb.js (771.79 KB)
js/vendor.6f983.js (477.57 KB)
js/platform.aee33.js (128.05 KB)
js/manifest.0559a.js (1.46 KB)
image.png

1.2 图表资源提前加载

  1. 图表资源加载分析
    ChartView组件的didMount中请求加载图表资源,导致图表空白1-2s才能渲染出来。
  2. 提前加载图表资源
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.options.sourcePath + '/com/fr/web/core/css/leaflet.css' %>"/>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.sourcePath + '/com/fr/mobile/js/appChart.js' %>"></script>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.sourcePath + '/com/fr/web/core/js/vancharts-all.js' %>"></script>
  1. 时间对比


    测试数据.png

1.3 打包区分不同入口

  1. 多入口分析
    项目支持多入口,不同入口打开同一个html页面。页面初始加载app.jsvendor.jsmanifest.js资源。当路由跳转到platformFRPage对应页面时,动态加载platform.jsFRPage.js
    当从模板页(cptfrm)进入项目时,FRPage.js仍通过动态路由的方式加载,加载时机有些延迟,增加了页面打开时间(客户反映提升不大)。
  2. 打包生成多个html页面,不同入口打开不同的html页面。当从登录页进入项目时,使用动态路由;当直接打开cpt或者frm模板时,不使用动态路由,提前加载对应js资源。
  3. 打包分析
Show chunks: All (4.55 MB)
js/sync.3bd37d4.js (1.98 MB)
js/app.cf1d197.js (1.23 MB)
js/FRPage.2285cd3.js (770.97 KB)
js/vendor.72c6bc3.js (478.69 KB)
js/platform.d5fb09a.js (128.02 KB)
js/manifest.43d0f75.js (1.46 KB)
image.png

1.4 代码分割

  1. Performance分析图
    有几个图表的表单.png
  2. 生成的app.js体积很大,但CommonsChunkPlugin只适用于多入口文件公共模块代码分割。本项目入口只有一个index.web.js,不符合CommonsChunkPlugin插件的应用场景。
  3. webpack升级
    (1) Cyclic dependency error
    解决:https://github.com/marcelklehr/toposort/issues/20
    (2) 其他见10.0 webpack升级遇到问题及解决方案。
  4. 代码分割
optimization: {
    splitChunks: {
        chunks: 'all',
        maxInitialRequests: 6,
        automaticNameDelimiter: '-',
        cacheGroups: {
            //base代码分割
            baseComponent: {
                test: /[\\/]public[\\/]base[\\/]components/,
                priority: 5,
                name: 'baseComponent'
            },
            appBase: {
                test: /[\\/]public[\\/]base/,
                priority: 0,
                name: 'appBase'
            },
            //react
            react: {
                test: /[\\/]node_modules[\\/](react\.*|redux\.*)|[\\/]lib[\\/]reactweb/,
                priority: -5,
                name: 'react',
            },
            vendors: {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                name: 'vendor'
            }
        }
    }
}
  1. 时间对比


    测试数据

2. 10.0版本打包优化

2.1 知识扩展

  1. babel-node 命令
  2. minimist 轻量级的命令行参数解析引擎
  3. define-plugin

2.2 问题分析

  1. happypack报错
    Cannot read property 'length' of undefined
if (resolve.length === 4) {
                ^
TypeError: Cannot read property 'length' of undefined
    at resolveLoader (...\node_modules\happypack\lib\WebpackUtils.js:138:17)
    at ...\node_modules\happypack\lib\WebpackUtils.js:126:7
    at ...\node_modules\happypack\node_modules\async\lib\async.js:713:13

分析:happypack 4.X不兼容webpack 4.X
解决:升级happypack5.X版本。

  1. file-loader报错
    Cannot read property 'fileLoader' of undefined
ERROR in ./node_modules/rs-styles/images/sampleStamp.png
Module build failed: TypeError: Cannot read property 'fileLoader' of undefined
    at Object.module.exports (/Users/uri/Documents/connect/dashboard/node_modules/file-loader/index.js:14:28)
    at Object.module.exports (/Users/uri/Documents/connect/dashboard/node_modules/url-loader/index.js:31:23)

分析:url-loader使用了旧版本的file-loader
解决:更新升级file-loader版本。
参考:https://github.com/webpack/webpack/issues/6419

  1. file-loader报错
    file-loader fails with JSON files in Webpack 4
Module parse failed: Unexpected token m in JSON at position 0
You may need an appropriate loader to handle this file type.
SyntaxError: Unexpected token m in JSON at position 0
    at JSON.parse (<anonymous>)
    at JsonParser.parse (/Users/jeremy/Documents/Development/webpack-file-loader-test/node_modules/webpack/lib/JsonParser.js:15:21)

分析:在webpack4.X版本中,默认支持json文件,无需loader处理。
解决:删除json处理的loader({test: /\.json$/,use: 'json-loader'})。
参考:https://github.com/webpack-contrib/file-loader/issues/259
4. 动态import()报错(☆☆)

ERROR in ./pages/Home/index.tsx 5:16
Module parse failed: Unexpected token (5:16)
You may need an appropriate loader to handle this file type.
| import { BeatLoader } from 'react-spinners';
| export const LoadableHomePage = Loadable({
>   loader: () => import(
|   /* webpackChunkName: "homepage" */
|   './page'),

方法一:npm update acorn --depth 20npm dedupe、删除node_modules、删除package.lock.jsonnpm install
方法二:npm install webpack@4.28.4
参考:https://github.com/webpack/webpack/issues/8656

  1. uglifyjs-webpack-plugin压缩动态import()语法报错
    分析:uglifyjs-webpack-plugin只支持ES5代码压缩,不支持ES6代码压缩。
    解决:使用terser-webpack-plugin替换掉uglifyjs-webpack-plugin
    参考:https://webpack.docschina.org/plugins/terser-webpack-plugin
  2. import()动态加载魔法注释webpackChunkName失效
    分析:①在项目中使用了module:metro-react-native-babel-preset,该presetbabelReactNative应用提供的,ReactNative应用默认使用它转化代码。②该preset支持动态import(),无需再使用@babel/plugin-syntax-dynamic-import。③ 该preset会在babel打包过程中删除注释内容。
    image.png

    解决:babel配置中添加comments: true
    参考:https://github.com/webpack/webpack/issues/4861
    https://babeljs.io/docs/en/options#comments
    https://www.npmjs.com/package/metro-react-native-babel-preset
  3. 打包生成的bundle中含有动态import()之外的js文件
    image.png

    分析:optimization.splitChunks在进行代码分割时,会默认将不同chunk引入的相同modules进行分割,避免这些代码重复打包到不同的bundle
    解决:配置optimization.splitChunks中的cacheGroups,将共用代码提取到自定义的group中。
optimization: {
  splitChunks: {
     chunks: 'all',
     cacheGroups: {
       commons: {
        minChunks: 2,
        priority: -20,
        name: 'commons',
      }
    }
  }
}
  1. webpack打包资源过大警告
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets: 
  js/commons.532a8.js (515 KiB)
  js/vendor.b2172.js (414 KiB)
  js/BIPage.dafcb.js (477 KiB)
  js/app.c64e5.js (1.2 MiB)

WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  app (1.77 MiB)
      js/react.97c29.js
      js/vendor.b2172.js
      js/app.c64e5.js

分析:webpack推荐打包生成的asset单个资源大小和入口资源大小在244 KiB以内。
隐藏:performance: { hints: false }
参考:https://github.com/webpack/webpack/issues/3486

  1. Tree Shaking为何不生效?
    分析:关于lodash,已经通过babel-plugin-lodash实现了Tree Shaking。对于业务模块,由于react-native使用的打包工具metro不支持Tree Shaking,因此不支持。
    解决:如果babel配置文件中使用module:metro-react-native-babel-preset,则可以支持Tree Shaking。但这样会导致react-native项目无法打包。
    参考:https://www.npmjs.com/package/metro-react-native-babel-preset
    https://github.com/facebook/metro/issues/227

2.3 打包优化

2.3.1 代码分割

  1. 同步代码分割
    通过optimization.splitChunks.cacheGroups,对basereactwebreact以及node_modules分别进行同步代码分割,生成appBase.jsreactweb.jsreact.js以及vendors.js
optimization: {
    splitChunks: {
        chunks: 'all',
        maxInitialRequests: 5,
        automaticNameDelimiter: '-',
        cacheGroups: {
            //同步模块代码分割
            appBase: {
                test: /[\\/]public[\\/]base/,
                priority: 0,
                name: 'appBase',
                chunks: 'initial'
            },
            reactWeb: {
                test: /[\\/]lib[\\/]reactweb/,
                priority: 0,
                name: 'reactWeb',
                chunks: 'initial'
            },

            //node_modules代码分割
            react: {
                test: /[\\/]node_modules[\\/](react\.*|redux\.*)/,
                priority: -5,
                name: 'react',
            },
            vendors: {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                name: 'vendor'
            }
        }
    }
}
  1. 异步代码分割
    通过动态import(),对LoginDirectoryReportPageFormPageBIPageWebPageChangePassword进行异步代码分割
Loadable({
    loader: () => import(/*webpackChunkName: "Login"*/'../../platform/view/Login'),
    loading: () => null,
})
  1. 打包
image.png
  1. 动态import()异步代码分割生成的一些bundler体积非常小。可以利用魔法注释中的webpackChunkName合并bundler

2.3.2 Base Tree Shaking

一. 问题分析

  1. base文件夹中绝大部分模块(modules)只在BIPageReportPageFormPage等异步Chunks中使用。为什么没有被打包到这些异步模块中,而是全部被同步代码分割出来了呢?
    即:为什么动态import()异步代码分割对base中模块modules失效?

如果没有对base的同步代码分割,这些base模块会被打包到入口app.js

  1. 分析
    一个模块如果被动态import()对应的异步chunk引入,同时被同步chunk引入,则该module会被打包进入同步chunk对应的bundler

二. 逐个切断依赖关系

  1. 使app chunk依赖树中不引入base/index模块。
    能够最大限度的降低app.jsbundler体积。但app中的依赖树比较复杂,逐个修改的工作量比较大。依赖树中只要有一条线指向base/index模块,就不能解决问题。
  2. 如何查找app chunk依赖树中哪些modules引入了base/index.js
    (1) 注释掉那些动态import()引入的chunks,只打包app对应的chunk
    (2) 通过分析工具可以查看module之间的依赖关系。
    (3) 找到依赖base/index.js的模块,修改依赖关系。
    image.png
image.png

app bundlerplatform bundler过滤base modules。在进入模板页之前,减少js bundler的体积。

三. babel插件

  1. 三个插件

(1) babel-plugin-module-resolver 插件

// Use this:
import MyUtilFn from 'utils/MyUtilFn';
// Instead of that:
import MyUtilFn from '../../../../utils/MyUtilFn';

可以实现webpack中的resolve.alias的功能。
参考:custom-aliases-in-react-native-with-babel
(2) babel-plugin-import

import { TimePicker } from "antd"
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/time-picker');

可以实现webpack中第三方库的Tree Shaking功能。
(3) babel-plugin-transform-imports

//Causes this code:
import { MyModule } from 'my-library';
import { App } from 'my-library/components';
import { Header, Footer } from 'my-library/components/App';
//to become:
import MyModule from 'my-library/MyModule';
import App from 'my-library/components/App';
import Header from 'my-library/components/App/Header';
import Footer from 'my-library/components/App/Footer';

可以实现第三方库与本地代码的Tree Shaking

第一个插件的作用是实现resolve.alias。后两个插件的作用是实现tree shaking。后两个插件和第一个插件共同使用时,后两个插件不生效。

  1. 最佳实践
    由于babel-plugin-module-resolver与另外两个插件共用使用时,另外两个插件不生效。因此,H5端使用webpack自身支持的resolve.alias和另外两个插件的其中之一(推荐babel-plugin-transform-imports)实现Tree ShakingApp端使用babel-plugin-module-resolver 插件实现alias
    image.png
  2. 注意事项

(1) 使用babel-plugin-transform-imports转换插件实现TreeShaking,需要保证一个module对应一个文件。

module.exports = function(importName, matches) {
    if(components.includes(importName)) {
        return `base/components/${importName}`
    }
    if(transMap[importName]) {
        return transMap[importName]
    }
    console.error(importName + ` must define the transform import format!`);
};

(2) 修改transformImport.js中转换函数后需要清除缓存文件

image.png

使用默认的缓存目录 node_modules/.cache/babel-loader,如果在任何根目录下都没有找到node_modules目录,将会降级回退到操作系统默认的临时文件目录。

2.3.3 CSS资源

  1. 项目中外部式样式和嵌入式样式比较少,对页面加载影响微乎其微。
  2. 因为与app共用代码。项目中绝大部分样式是通过StyleSheet.create({})以行内形式嵌入,导致打包输出的js文件体积增加。但如果不修改写法,行内样式无法抽出。

2.3.4 图片资源

  1. 图片资源预想的是将小图片转化为base64字符串或者使用CSS spirit。后来发现遇到问题如下:
    image.png

(1) 由于现有icon.js的写法。使用base64字符串会将没有使用到的小图标也打包到js文件中。造成js中图标体积约400KB
(2) CSS spirit图片拼接比较麻烦,现有的以webpack-spritesmithpostcss-sprites为代表的插件也是以css文件为基础,需要修改现有Icon组件的实现方式。即使解决了该问题,雪碧图拼接会将所有小图标拼成一个大图片,包括使用不到的图标。

3. 代码逻辑

  1. 区分不同入口逻辑
    js分包的角度来看,动态import()异步资源加载不适用于用户直接打开模板时的入口,js分包已经做了入口区分。从代码逻辑上来看,不同入口代码逻辑也应该做区分,减少直接打开模板生成的bundler体积。
  2. 异步阻塞逻辑优化
    页面渲染之前的异步回调会推迟页面初始渲染时间。

推荐阅读更多精彩内容

  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 705评论 0 2
  • 1 十次方需求分析与技术架构 1.1 十次方是个什么样的网站 《十次方》是程序员的专属社交平台,包括头条、问答、活...
    __豆约翰__阅读 133评论 0 3
  • babel 7 的使用的个人理解 最近看了很多关于babel的使用方法,大部分在一些点上都没有说明白,同时给出的代...
    zshawk1982阅读 10,252评论 13 31
  • 1. Node.js 介绍 npm的两层含义 npm 是一个网站,这个网站上托管了几十万个使用 JavaScrip...
    lemonzoey阅读 1,032评论 0 0
  • babel官网 babel 介绍 Babel 是一个通用的多用途 JavaScript 编译器。通过 Babel ...
    剃了胡子阅读 491评论 0 8