前言
最近花了一些时间在项目的性能优化上,背后做了很多工作,但是最后依然没有达到自己想要的结果,有些失望,但是还是记录下自己的执着。
性能优化总结:减少请求次数、减小资源大小、提高响应和加载速度、优化资源加载时机、优化加载方式。
性能分类
对前端工程性能的优化,我觉得可以分为两类:
- 站在用户视角的主观的可感知的性能。
- 站在开发者视角的可客观度量的性能。
感知性能
对于用户来说,用户的感知性能才是最重要的,简单讲,就是让用户感觉你的网站访问很快,并且感知性能没有衡量标准。
不过,凡事总有例外,如果一个页面的加载时间就会很长,我们也可以通过一些方式让用户觉得没有那么慢。
总之一句话,你的页面可以做的不快,但是你可以让你的用户觉得你很快。
客观性能
对于开发者来说,性能指标是可以客观度量的,我们可以通过一些手段来优化 Web 性能,使这些度量指标达到开发者设定的标准。
客观性能是指,从用户输入url开始,到下载、解析和执行所有资源以及最终绘制的整个过程的时间度量。
性能指标是个很复杂的标准,后续我会单独整理出一篇文章介绍性能指标。
构建优化
由于我司使用的是 @vue/cli,所以许多webpack配置脚手架已经帮你完成了,我就不叙述了,这里只讲基于 @vue/cli 做的一些优化配置
gzip 压缩
gzip
压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右。
//npm i -D compression-webpack-plugin configureWebpack: config => { const CompressionPlugin = require('compression-webpack-plugin') config.plugins.push(new CompressionPlugin()) }
去除 console.log
线上项目自然不应该被看到控制台的打印日志,所以我们需要将 console.log
都去除掉。
//npm i -D terser-webpack-plugin configureWebpack: config => { const TerserPlugin = require('terser-webpack-plugin') config.optimization.minimizer.push( new TerserPlugin({ extractComments: false, terserOptions: { compress: { drop_console: true } }, }) ) }
去除 SourceMap
由于打包后的文件经过了压缩、合并、混淆、babel编译后的代码不利于定位分析bug。
module.exports = { productionSourceMap: false, }
CDN减少打包体积
使用 cdn
文件来减少工程到打包体积,也可以按需加载。
在 /public/index.html 中引入需要的js和css文件
去掉 package.json 中对于 vue、element-ui 等相关资源的依赖
src/main.js ,去掉 vue、element-ui 等相关资源的 import 和 vue.use 这些语句
配置externals。由于使用 Vue Cli 3 默认配置,新建出来的项目没有了 build 目录,首先得在项目根目录下,新建 vue.config.js 文件,里面添加以下代码:
module.exports = { configureWebpack:{ externals:{ 'Vue': 'Vue', 'element-ui': 'element-ui', 'clipboard':'VueClipboard' } } }
预渲染
渲染方式分为三种,客户端渲染,服务端渲染,预渲染。
我们默认的开发方式是通过客户端渲染,但是客户端渲染页面内容,关键链路较长,首屏渲染会有一定延迟,并且对 SEO 非常不友好,对于C端的产品来说,是不可行的。
所以很多公司都会通过服务端渲染(SSR)或是 预渲染的方式来解决这两点问题,由于公司技术栈原因,我们采用预渲染的方式来做优化。
什么是预渲染?
简单说,就是将浏览器解析 javascript
动态渲染页面的这部分工作,在打包阶段就完成了,(只构建了静态数据)换个说法在构建过程中,webpack
通过使用 prerender-spa-plugin
插件生成静态结构的 html
。
// npm i -D prerender-spa-plugin configureWebpack: config => { const path = require('path') const PrerenderSPAPlugin = require('prerender-spa-plugin') config.plugins.push( new PrerenderSPAPlugin({ staticDir: path.join(__dirname, 'dist'), routes: ['/'], minify: { collapseBooleanAttributes: true, collapseWhitespace: true, keepClosingSlash: true, decodeEntities: true, sortAttributes: true, }, renderer: new PrerenderSPAPlugin.PuppeteerRenderer({ renderAfterDocumentEvent: 'render-event', renderAfterTime: 5000, // headless: false, }), }) ) }
注意:路由模式必须为 history
,如果不设置 history
模式,也能运行和生成文件,每个 index.html
文件的内容都会是一样的。
网络资源优化
Service Worker
ServiceWorker
是运行在浏览器后台进程里的一段 JS,它可以做许多事情,比如拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,其中最重要的作用之一就是离线资源缓存。
ServiceWorker
拥有对缓存流程丰富灵活的控制能力,当页面请求到 ServiceWorker
时,ServiceWorker
同时请求缓存和网络,把缓存的内容直接给用户,而后覆盖缓存,我司已经使用了 ServiceWorker
替换 HTTP缓存策略
。
注意:需要HTTPS才可以使用 ServiceWorker
http缓存
HTTP 缓存一般分为两类:强缓存(也称本地缓存) 和 协商缓存(也称304缓存)。
普通刷新会启用 协商缓存,忽略 强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源等情况下,浏览器才会启用 强缓存。
强缓存(200)
本地缓存是最快速的一种缓存方式,只要资源还在缓存有效期内,浏览器就会直接在本地读取,不会请求服务端。
协商缓存(304)
协商缓存,顾名思义是经过浏览器与服务器之间协商过之后,在决定是否读取本地缓存,如果服务器通知浏览器可以读取本地缓存,会返回304状态码,并且协商过程很简单,只会发送头信息,不会发送响应体。
缓存位置
缓存位置一般分为:Memory Cache(内存缓存)和 Disk Cache(硬盘缓存)
内存缓存:读取快、持续时间短、容量小
硬盘缓存:读取慢、持续时间长、容量大
缓存优先级
Service Worker -> Memory Cache -> Disk Cache -> Push Cache
HTTP2
HTTP2 四个新特性:
- 多路复用,无需多个TCP连接,因为其允许在单一的HTTP2连接上发起多重请求,因此可以不用依赖建立多个TCP连接。
- 二进制分帧,将所有要传输的消息采用二进制编码,并且会将信息分割为更小的消息块。
- 头部压缩,用HPACK技术压缩头部,减小报文大小
- 服务端推送,服务端可以在客户端发起请求前发送数据,换句话说,服务端可以对客户端的一个请求发送多个相应,并且资源可以正常缓存。
server { listen 443 ssl http2; }
注意:使用 http2 的前提是必须是 https。
资源预加载
简单说,提前加载资源,当用户需要查看时可直接从本地缓存中渲染。
总结:对当前页面需要的资源,使用 preload 进行预加载,对其它页面需要的资源进行 prefetch 预加载。
preload
preload
页面加载的过程中,在浏览器开始主体渲染之前加载。
<!-- 对sty1e.cs5和 index.js进行pre1oad预加载 --> <link rel="preload" href="style.css" as="style"> <link rel="preload" href="index.js" as="script">
prefetch
prefetch
页面加载完成后,利用空闲时间提前加载。
<!--对资源进行 prefetch预加载--> <link rel="prefetch" href="next.css"> <link rel="prefetch" href="next.js">
注意:vue-cli
默认开启 prefetch
,可在 vue.config.js
中全局禁用 prefetch
,再针对指定模块开启。
chainWebpack: config => { config.plugins.delete('prefetch') }
dns-prefetch
页面加载完成后,利用空闲时间提前加载。
<link rel="dns-prefetch" href="//example.com">
异步无阻塞加载JS
异步加载 js 文件,并且不会阻塞页面的渲染。
先来看一个普通的 script 标签解析过程。
<script src="a.js" ></script>
- 停止解析 document.
- 请求 a.js
- 执行 a.js 中的脚本
- 继续解析 document
defer
<script src="d.js" defer></script> <script src="e.js" defer></script>
- 不阻止解析 document, 并行下载 d.js, e.js
- 即使下载完 d.js, e.js 仍继续解析 document
- 按照页面中出现的顺序,在其他同步脚本执行后,DOMContentLoaded 事件前 依次执行 d.js, e.js。
async <script src="b.js" async></script> <script src="c.js" async></script>
- 不阻止解析 document, 并行下载 b.js, c.js
- 当脚本下载完后立即执行。(两者执行顺序不确定,执行阶段不确定,可能在 DOMContentLoaded 事件前或者后 )
webp
webp 是一种新的图片格式,它的体积只有只有 JPEG 的2/3,将图片资源大量换成 webp 格式可以加快请求的速度。
我司的图片资源大部分都放在阿里的 OSS 上,并且阿里提供了接口,可以在线将 png/jpeg 转为 webp 格式。
注意:webp 格式在浏览器兼容上还有一定的问题,所以需要判断浏览器是否支持 webp 格式哦。
function check_webp_feature(feature, callback) { var kTestImages = { lossy: "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA", lossless: "UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==", alpha: "UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==", animation: "UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA" }; var img = new Image(); img.onload = function () { var result = (img.width > 0) && (img.height > 0); callback(feature, result); }; img.onerror = function () { callback(feature, false); }; img.src = "data:image/webp;base64," + kTestImages[feature]; }
感知性能优化
loading 加载
江湖人称菊花图 ....
不过,现在对于加载的设计体验有了比菊花加载体验更棒的方法 。
骨架屏
骨架屏可以带来更好的用户体验,有很强的加载感。
可惜的是,ElementUI 并没有提供骨架屏组件,反观 Antd ,真香...
对比体验
第一个为骨架屏,第二个为菊花图,第三个为无优化,可以看到相比于传统的菊花图会在感官上觉得内容出现的流畅而不突兀,体验更加优良。
结尾
如果这篇文章帮助到了你,欢迎点赞和关注,搜索《海洋里的魔鬼鱼》加入我们的技术群一起学习讨论,共同探索前端的边界。