项目背景
该项目是一个公司内部使用的库,使用webpack进行打包。(webpack版本: 5.40.0)以往是使用npm的形式进行包管理,但由于项目打包后的体积较大,影响页面的加载速度,于是决定放弃使用npm包管理,将其打包成js文件,通过script标签进行引用。 对于npm包及js文件,各有其优劣势。
类型 | 优点 | 缺点 |
---|---|---|
npm包 | 便于做版本管理;引用方式简单 | 包体积过大时无法拆分;难以做加载优化 |
js文件 | 便于拆分模块,做加载优化;引用灵活性高 | 版本管理比较繁琐;调用者需要选择合适的时机加载js文件 |
由于是内部使用的包,并且对加载性能要求更高,所以可以使用js的方式
优化步骤
一、动态导入部分模块
将项目中一些并非一定用到并且体积相对较大的模块,采用webpack中动态导入的方式引用,从而减少首次加载的包体积。 例如修改前:
import module1 from './module1';
修改后:
// 此处即将要用到module1对象
const module1 = await import(
/* webpackChunkName: "module1" */ './module1'
);
这样webpack会将module1打包成一个独立的js文件。只有在你引入的时候才会去加载该文件。
webpack同时也支持预获取模块,就是在浏览器空闲的时候加载,而不是在你引入的时候就加载。这其实用到了<link>
标签rel
属性所支持的preload
和prefetch
值。
二、模块拆分
上一种方法只适用于并非立即会使用到的模块,但对于一些核心模块就无法使用该方法。例如我们项目中适用了three.js,该模块的体积也比较大,可以通过webpack配置optimization
将其打包成单独的js文件,
// ...
optimization: {
// ...
splitChunks: {
cacheGroups: {
three: {
test: /[\\/]node_modules[\\/](three)[\\/]/,
name: 'three',
chunks: 'all',
priority: 2,
},
},
},
},
其它体积较大的模块也可以分离出来。
三、修改模块导出方式
前面两个步骤主要是对模块拆分,目的是为了实现延迟加载和并行加载,这基本就能大幅提升加载速度
现在我们需要修改一下模块的导出方式。采用npm包管理的时候实际上打包后是一个CommonJS module
,即内部使用module.exports = myLibrary
导出,然后使用require('myLibrary')
方式引入。现在打包成一个js文件在浏览器直接引入,所以需要将myLibrary
暴露到全局对象下。(当然也可以使用AMD或其它浏览器支持的模块语法,但目前没使用这些方式),将webpack的output.library.type
从commonjs
改为umd
:
library: {
name: 'myLibrary',
type: 'umd',
},
四、部署js文件
我们需要把打包出来的产物部署到静态服务器。 配置webpack的publicPath:
// ...
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: `https://cdn.xxx.com/your/server/path`,
},
每次打包后将dist
目录上传到服务器路径https://cdn.xxx.com/your/server/path
下。我们目前是使用gitlab的CI/CD完成自动化部署。
五、使用新的方式调用
使用动态加载script的方式,以下是加载函数:
function loadScript(src) {
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script');
scriptEle.type = 'text/javascript';
// 动态脚本async默认为true, 设为false才能按顺序执行(并行加载)
scriptEle.async = false;
if (scriptEle.readyState) {
// IE
scriptEle.onreadystatechange = () => {
if (scriptEle.readyState == 'loaded' || scriptEle.readyState == 'complete') {
resolve();
}
};
} else {
scriptEle.onload = () => {
resolve();
};
scriptEle.onerror = () => {
reject(`The script ${src} is not accessible`);
};
}
scriptEle.src = src;
document.currentScript.parentNode.insertBefore(scriptEle, document.currentScript);
});
}
使用的是并行加载并按顺序执行,执行顺序为:
运行时文件 --> 被拆分出来的依赖 --> 主文件
六、更好利用缓存
使用哈希值作为打包后的文件名,以保证修改文件后缓存失效。
output: {
// ...
filename: '[name].[contenthash].js',
},
需要注意的是,当我们修改了其中一个文件,其它文件对应的打包产物的哈希值也可以改变.
举个例子,假如项目中存在三个模块并且都被打包成三个独立的js文件,模块依赖关系如下:
graph TD
module1 --> module2 --> module3
当我们修改了module1, 我们只期望module1对应的js文件名哈希值改变。而实际上这三个文件的文件名都发生改变。原因在于我们没有把运行时文件单独打包出来,一个文件的文件名改变会使另一个文件的内容改变,形成连锁反应。使用以下配置:
optimization: {
runtimeChunk: 'single',
}
这样打包后就会生成一个独立的运行时文件,这个文件也需要部署并加载(在其它文件执行前执行)。
这样我们在修改module1,就只有module1对应的js文件以及运行时文件的名称改变,从而优化缓存。