记录一次前端优化企业项目

记录一次前端优化企业项目

随着部门项目业务功能越来越多,发现打包速度慢、体积较大,想着能否针对 webpack 做优化。

优化打包速度

首先要找一个衡量速度的工具,webpack 插件speed-measure-webpack-plugin,可以测量所有 loader 花费的时间。

1
2
3
4
5
6
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

return smp.wrap({
// webpack文件返回的配置
});

然后执行yarn build,等待一会出结果。
优化loader前打包结果,总计打包时间287s

从图中可以看出,babel-loader、less-loader 执行时间较长,另外还有 eslint-loader,由于平时开发中编辑器都开启了 eslint 功能,另外在代码提交的时候有 pre-commit 也会做检测,由于项目过大每次打包都要让eslint-loader全量检测一遍,属实浪费时间性能,因此可以去掉,另外 2 个 loader 执行时间长,可能匹配内容过多,浪费不必要的搜索。

优化 loader

从这两个角度出发,首先去掉eslint-loader,然后优化其他 loader 的匹配规则。

项目中使用的.less、.js、.jsx,在源码中发现:

  • 存在不必要的 loader,例如 sass-loader 等项目用不到的类型文件,需要去掉。
  • 匹配了很多不需要匹配的文件后缀名,所以来说其他 loader 匹配文件都是浪费搜索性能的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 举例
{
rules: [
{
// 发现多出loader,都匹配不必要的文件类型,一并去掉
test: /\.(js|jsx)$/, // => 没必要匹配这么多文件, 优化前:(js|mjs|jsx|ts|tsx),
loader: require.resolve("babel-loader"),
},

// 类似这种的,可以去掉,因为项目并没有用到scss类型文件
{
test: /\.(scss|sass)$/,
loader: require.resolve("sass-loader"),
},
];
}

优化loader后打包结果,打包时间119s

时间从 287s 优化到 119s,速度提升了 58%。

优化体积

首先使用 webpack 插件webpack-bundle-analyzer分析各个包打包后的体积。

1
2
3
4
5
6
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

// webpack的plugins加入分析插件
{
plugins: [new BundleAnalyzerPlugin({ analyzerPort: 9999 })];
}

打包后发现第三方库moment、echarts、ant design、lodash体积比较大。

  • moment 体积大是因为把本地化内容页打包了。
  • Echarts 所有图形都打包了,其实只需要打包项目中用到的图的类型。
  • ant design 体积过大,由于时间控件内部采用 moment,应该替换成 dayjs。
  • lodash 默认打包了所有功能函数。

优化 moment

使用IgnorePlugin 忽略指定目录,moment会将所有本地化内容和核心功能一起打包,可以使用这个插件忽略本地化内容,在使用的时候再引入。

1
2
3
4
5
6
7
8
9
10
11
{
plugins: [
//忽略 moment 下的 ./locale 目录
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
];
}

// 在使用的时候
import moment from "moment";
import "moment/locale/zh-cn"; // 手动引入
moment.locale("zh-cn");

优化前后差别:

moment优化前
moment优化后

从图可以看出优化挺大的,264kb 到 60kb,体积减少了 77%。

优化 Echarts

用到什么类型图表,就引入什么类型的,没必要所有一并引入。

1
2
3
4
5
6
7
8
9
10
11
// 目前采用的是
import echarts from "echarts";

// 修改后,目前项目用的饼状图,和线图;按需引入
import echarts from "echarts/lib/echarts";
require("echarts/lib/component/title");
require("echarts/lib/component/tooltip");
require("echarts/lib/component/legend");
require("echarts/lib/component/grid");
require("echarts/lib/chart/pie");
require("echarts/lib/chart/line");

优化前,由图可以看出打包了所有图表类型
echart优化前

优化后
echart优化后

从 5.35m 优化到 253kb。

优化 ant design

在使用 Time-Picker 等时间控件的时候,默认使用的 moment,体积比较大,参考官网可以替换成 days.js。

官方示例

1
2
3
4
5
6
7
8
9
10
11
12
13
const AntdDayjsWebpackPlugin = require("antd-dayjs-webpack-plugin");

const config = {
plugins: [
new AntdDayjsWebpackPlugin({
preset: "antdv3",
}),
],
};

// index.js
import "dayjs/locale/zh-cn";
dayjs.locale("zh-cn");

尝试过发现并没有看出 ant design 体积变化差异很大,不过这也是一种优化的方向。

优化 lodash

目前采用lodash包,应该需要使用lodash-es模块化版本,支持 tree-shaking,可以减少打包体积。
不过目前项目都是用 lodash,并且没有单独使用函数,例如:import cloneDeep from 'lodash/cloneDeep',由于使用地方比较多,也不能每个页面都检查,所以打算采用lodash-webpack-plugin + babel-plugin-lodash两个 webpack 插件。

  • babel-plugin-lodash可以自动将 lodash 函数按需引入。
  • webpack-lodash-plugin去掉多余的功能代码,进一步减少体积。
1
2
3
4
5
6
7
8
9
10
import _ from "lodash";
import { add } from "lodash/fp";
const addOne = add(1);
_.map([1, 2, 3], addOne);

// 打包后
import _add from "lodash/fp/add";
import _map from "lodash/map";
const addOne = _add(1);
_map([1, 2, 3], addOne);

配置 webpack

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js文件
const LodashModuleReplacementPlugin = require("lodash-webpack-plugin");

const webpackConfig = {
plugins: [new LodashModuleReplacementPlugin()],
};

// .babelrc文件
{
plugins: [["lodash"]];
}

优化前后差别:

优化前
优化后

lodash 体积从 97kb 优化到 13kb,缩小了 86%的体积。

代码打包分块

使用代码分块之前,涉及到两个知识点:

  • 代码分离(Code Splitting):动态加载代码,在需要的时候再来加载,例如路由懒加载。
  • 打包分离(Bundle Splitting):主要是为更好的缓存,创建更多、更小的文件,可能很多页面共用了某一个模块功能,将其单独打包出来。

分包之前要考虑为什么要分包,能给项目带来什么优势?

  • 代码分离:如果不用路由懒加载,那就是一开始进入页面就会加载所有业务模块,导致首屏渲染速度很慢。
  • 打包分离:如果有一个体积很大的文件,只改了少量的代码,浏览器仍然要重新下载整个文件,如果分为两个或多个文件,浏览器只需要下载改动那个文件,其他未改动的部分从浏览器缓存中加载,可以提高加载速度。(所以来说代码分割提高访问速度,是与浏览器缓存息息相关。)

整体思路:需要怎么拆分,需要把不是每次都用到的依赖拆分出来,让其在使用的时候再根据页面按需加载。只有一个目的,就是要把用的比较少的公共依赖文件分割出来;引用次数较多的模块分割出来,避免每次都要重新加载那一部分代码。

着手操作:

  • 将项目框架代码分割,基本每个页面都会用到,例如:React、Ract-dom、React-dom-router、React-redux 等。
  • 将 Echarts 分割,目前了解到只有几个页面使用了图表,只需要访问指定页面再按需加载即可。
  • 项目中业务组件,基本都会用到,也需要单独分割出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const config = {
optimization: {
minimize: true,
splitChunks: {
chunks: "all",
//all: 不管文件是动态还是非动态载入,统一将文件分离。当页面首次载入会引入所有的包
//async: 将异步加载的文件分离,首次一般不引入,到需要异步引入的组件才会引入。
//initial:将异步和非异步的文件分离,如果一个文件被异步引入也被非异步引入,那它会被打包两次(注意和all区别),用于分离页面首次需要加载的包。

minSize: 30000, // 文件最小打包体积
minChunks: 1, // 文件最少引入次数,引入2次以上就会被打包

// 这里面打包配置,优先级高于外面的
cacheGroups: {
// 项目框架, 一开始就引入所有,因此使用all
vendors: {
chunks: "all",
test: /(react|react-dom|react-dom-router|react-redux|公司内部框架)/,
priority: 100,
name: "vendors",
},
// echarts只有部分页面使用,需要按需加载,因此需要用async
echartsVendors: {
chunks: "async",
test: /echarts/,
priority: 100,
name: "echartsVendors",
},
// 其他业务组件
commons: {
chunks: "all",
test: /(ta-biz|yss-biz)/,
minChunks: 2,
name: "commmon",
priority: 90,
},
},
},
},
};

总结

  • 打包分割是和浏览器的缓存机制存在联系的,主要是通过浏览器缓存没有变化的资源,提高加载速度。
  • 开发代码中尽量少用import * from 'xxx',没法进行 tree-shaking,导致打包体积变大。
  • 对于时间的处理,尽量使用体积更小的 dayjs,而不是体积较大的 moment,如果必须要使用 moment,一定要对本地化配置做处理,减少打包体积。
  • 对于lodash,尽量使用模块版本的lodash-es,可以利用 tree-shaking,减少打包体积。