前端性能优化实战 从 30s 到 2s - zptime/blog GitHub Wiki
最近做的管理端项目,发现首屏加载速度贼慢,对于 5G 快网速时代,这真的是个大问题呀,所以进行了一系列的优化。
介绍了前端性能分析工具,如 Performance 面板、Lighthouse、PageSpeed Insights;也介绍了包分析工具,如 webpack chart、webpack bundle analyzer、webpack visualizer 等;进行了项目优化,如 Echarts 按需引入,Moment 压缩配置,ramda、ant design vue、ant design vue icons、Quasar 等的按需引入,gzip 压缩配置等等
管理端项目是用easywebpack-vue
搭建起来的,采用的是服务端渲染,用到了quasar
和ant design vue
两个组件库
EasyWebpack 基本介绍:https://www.yuque.com/easy-team/easywebpack/home
Asset 渲染模式:https://www.yuque.com/easy-team/egg-vue/asset
Quasar:http://v0-16.quasarchs.com/
Ant Design Vue:https://antdv.com/docs/vue/introduce-cn/
Google 浏览器 Performance 面板就是一个很强大的工具,可以进行多方面的分析
Lighthouse 是 google 一个开源的自动化工具,展示了白屏、首屏、可交互时间等性能指标和 SEO、PWA 等。
运行 Lighthouse 的方式有两种:一种是作为 Chrome 扩展程序运行;另一种作为命令行工具运行。
// 安装命令:
npm install -g lighthouse
// 审查某个网页:
lighthouse https://www.imooc.com/
// 帮助查看
lighthouse --help
//导出HTML格式
lighthouse https://www.imooc.com/ --output html
查询网址:https://developers.google.com/speed/pagespeed/insights/
分析文档:https://developers.google.com/speed/docs/insights/about
Google Page Speed Insight(PSI)是一款旨在优化所有设备上的网页、提高网页加载速度的工具。
例如,淘宝首页分析:
网址:http://alexkuz.github.io/webpack-chart/
介绍:上传stat.json
文件,即可生成整体的项目文件层次结构图。点击任何一个区域,进入子目录文件分析,这个工具只能提供一个整体的文件依赖结构,是一个类饼图结构。
网址:http://webpack.github.io/analyse/
介绍:上传stat.json
文件,即可生成整个项目文件个数(包括 vue 文件,js,css, image, chunk 等)、文件大小、文件依赖关系统计,可以很方便找出筛选出大的 js、image、css,然后进行针对性的优化,信息很全,对优化文件大小有很大帮助。
网址:https://webpack.jakoblind.no/optimize/
介绍:分析你的 bundle 包大小,并为你提供可操作的改进措施建议,以减少 bundle 体积大小。
网址:https://chrisbateman.github.io/webpack-visualizer/
介绍:生成一个饼状图,可视化 bundle 内容。以查看哪些模块正在占用空间,哪些可能是重复的
构建包分析工具:analyzer(webpack-bundle-analyzer) 和 stats(stats-webpack-plugin) , 默认用 analyzer 插件。
如果提示“zsh: command not found: easy”
,则运行如下命令,全局安装一下,就可以在命令行中使用 easywebpack
or easy
命令, 比如 easy init
, easy build
, easy dev
, easy print
等
$ sudo npm i easywebpack-cli -g
如下命令会对生产环境进行打包构建,移除开发辅助代码, 压缩 js/css/image 以及 hash。会自动打开两个地址,可以查看包的结构、大小、依赖关系等
$ easy build prod -s
如下命令会对生产环境进行打包构建,而且会生成生成client_stats.json
文件,就可以用上面介绍的分析工具进行分析了。
$ easy build prod -s stats
使用 webpack analyse
分析如下:
如下就是优化前的分析,可以看出:
- vendor 文件占比情况:moment > ramda > runtime > vue-router > vuex
- [entry].js 文件占比情况:ant design vue > main.js > quasar > @ant design icons
webpack-bundle-analyzer 模块分析如下:
webpack bundle optimize helper 入口文件分析如下所示:
未优化前,首页的 lighthouse 分析报告:
如下就是优化前的分析,可以看出:
- vendor 文件占比情况: runtime > moment > vue-router > vuex > axios
- [entry].js 文件占比情况:main.js > ant design vue > @ant design icons > quasar
入口文件包大小减少 0.39MB,即 400KB
。
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from "echarts/core";
// 引入各种图表,图表后缀都为 Chart
import { BarChart, LineChart, PieChart } from "echarts/charts";
// 引入提示框,标题,直角坐标系等组件,组件后缀都为 Component
import {
TitleComponent,
TooltipComponent,
ToolboxComponent,
GridComponent,
LegendComponent,
AxisPointerComponent,
DatasetComponent,
} from "echarts/components";
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { SVGRenderer } from "echarts/renderers";
// 注册必须的组件
echarts.use([
BarChart,
LineChart,
PieChart,
TitleComponent,
TooltipComponent,
ToolboxComponent,
GridComponent,
LegendComponent,
AxisPointerComponent,
DatasetComponent,
SVGRenderer,
]);
export default echarts;
Moment 默认支持多种语言,但是本系统只用到了中文,故此可排除其他语言。使用如下配置后,vendor 减少了约 200kb
。
其实 Moment.js 本身就挺大的,有 232KB,可以换成 Day.js,只有 12KB。
dayjs 替换 momentJS 文档:https://github.com/ant-design/antd-dayjs-webpack-plugin
// 修改webpack.config.js文件
plugins: [
// 排除i18n其他语言,仅加载中文 `moment/locale/zn-cn.js`
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
];
按需引入后,vendor 减少了 92kb
。
// 安装依赖
npm install babel-plugin-ramda --save
// 配置babel.config.js文件
module.exports = {
plugins: [
// "ramda",
// 或者
["ramda", { "useES" : true }]
],
};
因为需要对组件库进行定制化改造,会直接修改源码,所以直接将组件库代码下载到本地项目中,相关的优化配置也会稍有不同。
使用 ant design vue 按需引入,入口文件减少了 0.65MB,即 666KB
。
// 安装依赖
npm install babel-plugin-import --save-dev
// 配置webpack.config.js文件
module.exports = {
plugins: [
[
"import",
// 默认js路径是[libraryName]/[moduleType]/[componentName]
// 默认css路径是[libraryName]/[moduleType]/[componentName]/style
{
// 引入库名称,默认是ant-design-vue
libraryName: "@ui/ant-design-vue",
// 来源目录,默认是“es”
libraryDirectory: "",
// style:css-识别css文件;tree-识别less文件
style: true,
// // 自定义组件路径
// customName: (name) => {
// return `ant-design-vue/${name.toLowerCase()}`
// }
// // 自定义组件库样式
// style: (name) => {
// // name为组件所在的路径,按需加载样式配置提取对应组件名称的css即可
// return `@/ant-design-vue/${name}/style`
// },
},
],
],
};
// 配置后的实现
import { Button } from "@ui/ant-design-vue"
// 等价于
import "@ui/ant-design-vue/button/style/index.less"
import Button from "@ui/ant-design-vue/button/index.js"
ant design vue icons 图标库也挺大的,实际项目中只用到了几个,大部分用的都是自己的图标,所以将图表库按需引入还是很有必要的。但是这个工作量稍微有点大,要对照处理,容易漏掉。
配置其实挺简单的,就是将 ant design vue icons 图标库的加载路径重新命名一下。去除所有图标,可以减少 0.47MB,即 480KB
。
(1)配置 webpack.config.js 文件
module.exports = {
alias: {
"@ant-design/icons/lib/dist$": resolve("./utils/icons.js"),
},
};
(1)新增 icon.js 文件
export { default as CalendarOutline } from "@ant-design/icons/lib/outline/CalendarOutline";
export { default as SettingOutline } from "@ant-design/icons/lib/outline/SettingOutline";
export { default as GithubOutline } from "@ant-design/icons/lib/outline/GithubOutline";
export { default as CopyrightOutline } from "@ant-design/icons/lib/outline/CopyrightOutline";
Quasar 按需引入配置,入口文件减少了0.51MB,即522KB
Quasar 主要用于外层框架的构建,例如左侧菜单和头部导航等;内部的页面开发全部用的 ant design vue。后期可以慢慢将 Quasar 替换掉,减少一个前端框架。
// 配置babel.config.js文件
module.exports = {
plugins: [
[
"transform-imports",
{
quasar: {
transform: require("quasar/dist/babel-transforms/imports.js"),
preventFullImport: true,
},
},
],
],
};
前端通过 webpack,grunt,或者 gulp 打包压缩,基本能压缩 50%以上,服务端开启 gzip 压缩可以在之前的基础上再压缩 50%以上,所以这个真的是一定要开启的呀。
经过 gzip 压缩,文件大小可以变为之前的 30%甚至更小
。本次实践中,入口文件由 3.6MB 变为 1MB,减少了 72%,事件也由 30s 减为 2s 左右了
(1)查看客户端是否开启 gzip 压缩:请求头中有个 Accept-Encoding
来标识对压缩的支持。
(2)查看服务端开启 gzip 压缩:在 http 响应头,我们可以看到 content-encoding:gzip
(3)前端开启 gzip 压缩如下,要和服务端进行配合,服务端也要配置 gzip 才行:
// 配置webpack.config.js文件
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = {
plugins: [
new CompressionPlugin({
// filename: '[path].gz[query]', // 目标文件名
algorithm: "gzip", // 使用gzip压缩
test: /\.js$|\.css$|\.html$|\.eot?.+$|\.ttf?.+$|\.woff?.+$/, // 压缩文件类型
threshold: 102400, // 对超过100k的数据压缩
minRatio: 0.8, // 最小压缩比达到0.8时才会被压缩
}),
],
};
dll 配置官网:https://www.yuque.com/easy-team/easywebpack/dll
对于一些常用的,基本不咋修改的第三方文件,我们可以提前打包好,单独生成一个包。后面再打包的时候,直接用这个就行了,就提高了 webpack 的打包速度。
如果是一般的项目,需要自己进行 webpack dll 的配置,而 easywebpack 已经在底层集成好了,我们只需要如下简单配置一下就行。
dll 配置支持:默认提取的文件为 vendor.js.
// webpack.config.js
module.exports = {
dll: ["vue", "vuex", "vue-router", "axios", "js-cookie", "moment"],
}
要进行合理的代码分割,减少入口文件大小。默认配置只会生成 runtime,common,[entry].js 三个 js 文件。
module.exports = {
optimization: {
namedModules: true,
namedChunks: true,
runtimeChunk: {
name: "runtime",
},
splitChunks: {
name: false,
chunks: "all", // all(全部), async(异步的模块),initial(同步的模块)
minSize: 20000, // 表示文件大于1k的时候才对它进行打包
minChunks: 2, // 当某个模块满足minChunks引用次数时,才会被打包。
maxAsyncRequests: 5, // 在打包某个模块的时候,最多分成5个chunk,多余的会合到最后一个chunk中
maxInitialRequests: Infinity,
automaticNameDelimiter: "~", // 当vendors或者default中的filename不填时,打包出来的文件名就会带~
cacheGroups: {
default: false,
vendors: {
// 将从node_modules中引入的模块统一打包到common.js文件中
name: "common",
chunks: "all",
minChunks: 2,
test: /[\\/]node_modules[\\/]/,
},
styles: {
// css统一打包到common.css文件中
name: "common",
chunks: "all",
minChunks: 2,
test: /\.(css|less|scss|stylus)$/,
enforce: true,
priority: 50,
},
},
},
},
};
DNS Prefetch
(DNS 预解析):根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短 DNS 解析时间,来提高网站的访问速度。
浏览器自动解析:浏览器引擎在解析 HTML 页面的时候,会自动获取当前页面所有的 a 标签 href 属性当中的域名,然后进行DNS Prefetch
。这个解析过程是与用户浏览网页并行处理的。但是为了确保安全性,在 HTTPS 页面中没有开启 DNS Prefetch
。
<!-- 开启DNS Prefetch -->
<meta http-equiv="x-dns-prefetch-control" content="on" />
JavaScript 引擎是独立于渲染引擎存在的,即会阻塞 HTML 解析,也会阻塞 CSS 解析,因此可以改变 JavaScript 的加载方式或者加载时机来进行优化:
- 尽量将 js 文件放在 body 底部
- 使用 defer 和 async 来避免不必要的阻塞
async
模式:异步加载,不会阻塞浏览器的加载,加载成功后,会立即执行。使用多个 async
模式引入的多个文件执行顺序是不确定的。
defer
模式:异步加载,执行会被推迟。等整个文档解析完成后,会执行所有有 defer
加载的 js 文件代码。使用多个 defer 模式引入的多个文件从上往下顺序执行。
<!DOCTYPE html>
<html lang="en">
<head>
<script async src="m-es6-promise.min.js"></script>
<script async src="m-polyfill-v5.3.js"></script>
</head>
<body>
<script defer>
/* 百度统计代码 */
window._hmt = window._hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?{{hmtKey}}";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</body>
</html>