一: 前端优化概览
web 前端的性能优化,个人认为大概可以分为两个阶段,一个是在开发过程中的优化,一个是在打包后的静态文件进行优化.
开发过程中的优化
包含防抖节流,逻辑优化,接口内容优化(如减少接口调用数,合理控制每个接口返回数据数量,避免接口调用时间过长)等等,另外,在开发过程中,还可以通过使用字体图标,svg图片,精灵图等减少静态资源的大小,来减少首屏和路由页面的加载时间.
打包后静态文件的优化
这个阶段的优化,其目标就是 减少静态文件的体积,或者减少首页加载的静态文件的数量来减少首页的加载速度.
本文主要针对打包后静态的文件的优化进行总结,开发过程中的优化后续再总结.
打包后静态文件的优化点
个人认为主要包含如下几个点:
- 对打包后的静态文件进行分包处理
- 减小包的体积(代码压缩)
- prefetch 和 preload(预加载,预获取)
- CDN服务(加快用户下载包的速度)
二:分包处理静态文件
webpack 针对静态的文件的分包 主要分为以下几种形式
- 入口起点: 使用entry 配置多入口 手动分离代码
- 防止重复: 使用Entry Dependcies 或者SpiltChunksPlugin 去重和分离代码
- 动态导入: 通过模块的内联函数调用来分离代码(如vue 和 raact 的路由懒加载)
分包处理静态文件能提升性能的原理是,在应用中有些页面 并不是用户一打开页面就必须要加载的,但如果我们把所有的文件打包成一个静态文件,那必须要下载这些不必须的代码,导致首页加载时间过长,如果我们把这部分代码隔离出来,单独打包,让用户在需要的时候再去下载,这样就可以优化首屏加载时间,也能提高用户体验.
分包方式一: webpack 多入口依赖
首先要说明的是使用这种形式进行分包的场景很少.
概念
多入口指的是,我们在进行webpack 打包配置的时候,配置多个入口文件,让webpack 分别从不同的入口进行打包,这样每个入口打包的体积就会大大减小,而用户只会从一个入口进入,这样就大大减小了用户下载静态文件的体积.
用法:
- 在webpack 配置文件中配置多个入口:
const path = require('path');
module.exports = {
entry: {
main: './src/main.js',
admin: './src/admin.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
- 配置各个入口文件 共同依赖的第三方库
这里首先需要把每个入口的配置由字符串转化为一个对象,在这对象中,
- import 属性的值代表 入口文件的路径
- dependOn 属性的值 代表这个入口 要使用那个共享的 配置
shared 代表一个共享配置,shared 可以有多个,命名形式可以是shared1,shared2,...
shared 值代表这个共享配置,具体共享那些 第三方库
const path = require('path');
module.exports = {
entry: {
main:{
import: './src/main.js',
dependOn: 'shared'
}
admin:{
import: './src/admin.js',
dependOn: 'shared'
},
shared: ['axios']
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true
}
};
分包方式二: 动态导入
动态导入是我们常用的分包方式,webpack 提供了两种动态导入的方式:
- 第一种, 使用ECMAScript 中的 import() 语法来完成,也是目前最推荐的方式
- 第二种, 使用webpack 遗留的 require.ensure,目前已经不推荐使用.
比如 我们有一个模块 bar.js
- 该模块我们希望 在代码的运行过程中来加载它(比如判断一个条件成立时加载)
- 因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件
- 这样可以保证不用到该内容时, 浏览器不需要加载和处理该文件的js代码
- 这个时候我们就可以使用动态导入
仔细想想,其实我们在开发vue,react 项目时, 使用动态路由懒加载的形式 加载某个页面时,就是使用的动态导入,只不过是不清楚这个概念而已.
动态导入 是webpack 默认提供的功能,不需要我们做特殊的配置就可以使用. 这里就贴出一小段代码,大家明白具体用法就行
当点击btn 按钮时,加载我们打包的about 组件
btn.onclick = function() {
import('./router/about').then(res =>{
console.log(res)
})
}
动态文件导入的文件命名:
因为动态导入通常是一定会打包成独立的文件的,所以并不会在cacheGroups中进行配置
那么他们的命名我们可以在ouput 中,通过chunkFilename属性来命名;
output :{
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
chunkFilename: 'chunk_[name].js'
}
但是,你会发现默认情况下我们获取到的[name]会过长和不利于查看 这个时候,如果我们希望修改name 的值,可以通过**magic comments(魔法注释)**的方式来修改
import(/* webpackChunkName: “about” */ './router/about').then(res =>{
console.log(res)
})
分包方式三: splitChunks
另外一种分包的模式是splitChunk,它底层使用SplitChunksPlugin来实现的.
- 因为该插件webpack 已经默认安装和集成,所以我们并不需要单独安装就可以直接使用该插件;
- 只需要提供SplitChunksPlugin 相关的配置信息即可.
Webpack 提供了SplitChunksPlugin默认的配置,我们也可以手动来修改它的配置
- 比如默认配置中,chunks仅仅针对于异步(async )请求,我们也可以设置为all,这样就可以将所有的文件根据我们配置的条件进行分包.
optimization: {
splitChunks: {
chunks: 'all'
}
}
另外,我们还可以修改maxSize(包达到多少byte时,就进行分包),minSize(最小的包要多少byte) 详细配置可以查看:webpack文档
分包其他配置
chunkIds
采用id 占位符 给分包命名时,有时候觉得id 的默认生成机制太长,不利于查看,这个时候可以通过chunkIds 属性配置
optimization.chunkIds配置用于 告知webpack 模块的id采用什么算法生成
有三个比较常用的值:
- natural: 按照数字顺序使用id;
- named: develpment 下的默认值,一个可读的名称的id
- deterministic:确定性的,在不同的编译中不变的短数字id
- 在webpack 4 中是没有这个值的.
- 那个时候如果使用naatural,那么会在一些编译发生变化时,产生一些问题
最佳实践:
- 开发过程中,推荐使用named
- 打包过程中,推荐使用deterministic
runtimeChunk配置
optimization.runtimeChunk 是配置runtime 相关的代码是否抽取到一个单独的chunk中的配置.
抽离出来后,有利于浏览器缓存的策略:
- 比如我们修改了 业务代码(main),那么runtime 和 component,bar 的chunk 是不需要重新加载的.
可以设置的值:
不为对象时:
- true/multiple: 针对每个入口打包一个runtime 文件
- single: 打包一个runtime 文件
为对象时: name 属性决定runtimeChunk的名称
optiimization: {
runtimeChunk:{
name: "runtime"
}
}
三:代码压缩
对于代码压缩,Webpack 可以通过插件来实现,比如使用用 TerserWebpackPlugin 来压缩JavaScript代码。
以下是一个简单的配置示例,展示了如何在 Webpack 中使用 TerserWebpackPlugin 来压缩代码:
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserWebpackPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除console语句
},
},
extractComments: false, // 不从代码中提取注释
})],
},
};
四:prefetch 和 preload
webpack v4.6.0 + 增加了对预获取和预加载的支持.
在声明时,使用下面这些内置指令,来告知浏览器:
- prefetch(预获取): 将来某些导航下可能需要的资源
- preload(预加载): 当前导航下可能需要的资源
import(
/* webpackChunkName: “component”*/
/* webpackPreload: true*/
“./component”
).then(({default: component})=>{
})
与 prefetch 指令相比, preload 指令有许多不同之处:
- preload chunk 会在 chunk加载时,以并行方式开始加载.prefetch chunk 会在父 chunk 加载结束后开始加载.
- preload chunk 具有中等优先级,并立即下载. prefetch chunk在浏览器闲置时下载.
- preload chunk 会在父 chunk 中立即请求,作用于当下时刻. prefetch chunk 会作用于未来的某个时刻.
五:CDN 服务
什么是CDN
CDN 称之为内容分发网络
- 它是指通过相互连接的网络系统,利用最靠近每个用户的服务器
- 更快,更可靠的将音乐,图片,视频,应用程序及其他文件发送给用户
- 来提供高性能,可拓展性及低成本的网络内容传递给用户
CDN 的使用方式
在开发中,我们使用CDN 主要是两种方式:
- 方式一: 打包所有的静态文件,放到CDN 服务器,用户所有资源都是通过CDN 服务器加载的
- 方式二: 一些第三方资源放到CDN 服务器上
购买CDN 服务器
如果所有的静态资源都想要放到CDN 服务器上,我们需要购买自己的CDN 服务器;
- 目前阿里,腾讯,亚马逊,Google 等都可以购买CDN服务器
- 我们可以直接修改publicPath,在打包时添加上自己的CDN地址
output: {
path: path.resolve(__dirname, 'dist'),
filename: “[name].bundle.js”,
publicPath: “https://wz.com/cdn”,
chunkFilename:“[name].[hash:6].chunk.js”
}
第三方库放在CDN 服务器
通常一些比较出名的开源框架都会将打包后的源码放到一些比较出名的,免费的CDN服务器上:
- 国际上使用比较多的是unpkg,JSDelivr,cdnjs;
- 国内也有一个比较好用的CDN是 bootcdn
在项目中,我们如何去引入这些CDN呢?
- 第一,在打包的时候我们不需要对类似于ladash或者dayjs 这些库进行打包;
- 第二,在html模块中,我们需要自己加入对应的CDN服务器地址;
具体用法:
第一步:我们可以通过webpack配置,来排除一些库的打包:
externals: {
lodash: "_",
// key 属性名: 排除的框架的名称
// value值: 从CDN地址 请求下来的js 中提供的对应的名称
dayjs: "dayjs"
}
第二步: 在html 模块中,加入CDN服务器地址:
<script src="https://cdn.jsdelivr.net/npm/dayjs/dayjs.min.js"></script>
`< script src="https://cdn.bootcss.com/lodash.js/4.17.15/lodash.core.min.js">< /script >`