企鹅辅导 H5 页面在长期迭代过程当中,逐渐累积了一些性能问题,致使页面加载、渲染速度变慢。为了提高用户体验,近期针对页面加载速度,渲染速度作了专项优化,本文是对这次优化的实践总结。分析过程比较细致,但愿能给性能分析经验欠缺的同窗一些帮助。javascript
H5 项目是企鹅辅导的核心项目,已迭代四年多,包括了课程详情页/老师详情页/报名页/支付页面等页面,构建产物用于企鹅辅导 APP/H5(微信/QQ/浏览器),迭代过程当中了也累积了一些性能问题致使页面加载、渲染速度变慢, 为了提高用户体验,近期启动了“H5 性能优化”项目,针对页面加载速度,渲染速度作了专项优化,下面是对本次优化的总结,包括如下几部份内容。css
企鹅辅导 H5 采用的性能指标包括:html
1.页面加载时间:页面以多快的速度加载和渲染元素到页面上。前端
2.加载后响应时间:页面加载和执行js代码后多久能响应用户交互。java
3.视觉稳定性:页面元素是否会以用户不指望的方式移动,并干扰用户的交互。node
项目使用了 IMLOG 进行数据上报,ELK 体系进行现网数据监控,Grafana 配置视图,观察现网状况。react
根据指标的数据分布,能及时发现页面数据异常采起措施。webpack
现网页面状况:ios
能够看到进度条在页面已经展现后还在持续 loading,加载时间长达十几秒,比较影响了用户体验。nginx
根据 Google 开发文档 对浏览器架构的解释:
当导航提交完成后,渲染进程开始着手加载资源以及渲染页面。一旦渲染进程“完成”(finished)渲染,它会经过IPC告知浏览器进程(注意这发生在页面上全部帧(frames)的 onload 事件都已经被触发了并且对应的处理函数已经执行完成了的时候),而后UI线程就会中止导航栏上旋转的圈圈
咱们能够知道,进度条的加载时长和 onload 时间密切相关,要想进度条尽快结束就要 减小 onload时长。
根据现状,使用ChromeDevTool做为基础的性能分析工具,观察页面性能状况
Network:观察网络资源加载耗时及顺序
Performace:观察页面渲染表现及JS执行状况
Lighthouse:对网站进行总体评分,找出可优化项
下面以企鹅辅导课程详情页为案例进行分析,找出潜在的优化项
(注意使用Chrome 隐身窗口并禁用插件,移除其余加载项对页面的影响)
一般进行网络分析须要禁用缓存、启用网络限速(4g/3g) 模拟移动端弱网状况下的加载状况,由于wifi网络可能会抹平性能差距。
能够看到DOMContentLoaded的时间在 6.03s ,但onload的时间却在 20.92s
先观察 DOMContentLoaded 阶段,发现最长请求路径在 vendor.js ,JS大小为170kB,花费时间为 4.32s
继续观察 DOMContentLoaded 到 onload 的这段时间
能够发现onload事件被大量媒体资源阻塞了,关于 onload 事件的影响因素,能够参考这篇文章
结论是 浏览器认为资源彻底加载完成(HTML解析的资源 和 动态加载的资源)才会触发 onload
结合上图 能够发现加载了图片、视频、iFrame等资源,阻塞了 onload 事件的触发
Network 总结
使用Performance模拟移动端注意手机处理器能力比PC差,因此通常将 CPU 设置为 4x slowdown 或 6x slowdown 进行模拟
观察几个核心的数据
能够看到 LCP、DCL和 Onload Event 时间较长,且出现了屡次 Layout Shift。
要 LCP 尽可能早触发,须要减小页面大块元素的渲染时间,观察 Frames 或ScreenShots 的截图,关注页面的元素渲染状况。
能够经过在 Experience 行点击Layout Shift ,在 Summary 面板找到具体的偏移内容。
能够看到页面有大量的Long Tasks须要进行优化,其中couse.js(页面代码)的解析执行时间长达800ms。
处理Long Tasks,能够在开发环境进行录制,这样在 Main Timeline 能看到具体的代码执行文件和消耗时长。
Performance 总结
使用ChromeDevTool 内置 lighthouse 对页面进行跑分
分数较低,能够看到 Metrics 给出了核心的数据指标,这边显示的是 TTI SI TBT 不合格,LCP 须要提高,FCP 和 CLS 达到了良好的标准,能够查看分数计算标准
同时 lighthouse 会提供一些 优化建议,在 Oppotunities 和 Diagnostics 项,能看到具体的操做指南,如 图片大小、移除无用JS等,能够根据指南进行项目的优化。
lighthouse 的评份内容是根据项目总体加载项目进行打分的,审查出的问题一样包含Network、Performance的内容,因此也能够看做是对 Network、Performance问题的优化建议。
Lighthouse 总结
刚才是对线上网页就行初步的问题分析,要实际进行优化和观察,须要进行环境的模拟,让优化效果能更真实在测试环境中体现。
代理使用:whistle、charles、fiddler等
本地环境、测试环境模拟:nginx、nohost、stke等
数据上报:IMLOG、TAM、RUM等
前端代码打包分析:webpack-bundle-analyzer 、rollup-plugin-visualizer等
分析问题时使用本地代码,本地模拟线上环境验证优化效果,最后再部署到测试环境验证,提升开发效率。
Network 中对页面中加载的资源进行分类
第一部分是影响 DOM解析的JS资源,能够看到这里分类为 关键JS和非关键JS,是根据是否参与首面渲染划分的
这里的非关键JS咱们能够考虑延迟异步加载,关键JS进行拆分优化处理
JS 文件数量8个,整体积 460.8kB,最大文件 170KB
vendor.js 170kB(gzipd) 是全部页面都会加载的公共文件,打包规则是 miniChunks: 3,引用超过3次的模块将被打进这个js
分析vendor.js的具体构成(上图)
以string-strip-html.umd.js 为例 大小为34.7KB,占了 vendor.js的 20%体积,但只有一个页面屡次使用到了这个包,触发了miniChunks的规则,被打进了vendor.js。
同理对vendor.js的其余模块进行分析,iosSelect.js、howler.js、weixin-js-sdk等模块都只有三、4个页面/组件依赖,但也一样打进了 vendor.js。
由上面的分析,咱们能够得出结论:不能简单的依靠miniChunks规则对页面依赖模块进行抽离打包,要根据具体状况拆分公共依赖。
修改后的vendor根据业务具体的需求,提取不一样页面和组件都有的共同依赖(imutils/imlog/qqapi)
vendor: {
test({ resource }) {
return /[\\/]node_modules[\\/](@tencent\/imutils|imlog\/)|qqapi/.test(resource);
},
name: 'vendor',
priority: 50,
minChunks: 1,
reuseExistingChunk: true,
},
复制代码
而其余未指定的公共依赖,新增一个common.js,将阈值调高到20或更高(当前页面数76),让公共依赖成为大多数页面的依赖,提升依赖缓存利用率,调整完后,vendor.js 的大小减小到 30KB,common.js 大小为42KB
两个文件加起来大小为 72KB,相对于优化前体积减小了 60%(100KB)
course.js 101kB (gzipd) 这个文件是页面业务代码的文件
观察上图,基本都是业务代码,除了一个巨大的** component Icon,占了 25k**,页面文件1/4的体积,但在代码中使用到的 Icon 总共才8个
分析代码,能够看到这里使用require加载svg,Webpack将require文件夹内的内容一并打包,致使页面 Icon 组件冗余
如何解决这类问题实现按需加载?
按需加载的内容应该为独立的组件,咱们将以前的单一入口的 ICON 组件(动态dangerouslySetInnerHTML)改为单文件组件模式直接引入使用图标。
但实际开发中这样会有些麻烦,通常须要统一的 import 路径,指定须要的图标再加载,参考 babel-plugin-import,咱们能够配置 babel 的依赖加载路径调整 Icon 的引入方式,这样就实现了图标的按需加载。
按需加载后,从新编译,查看打包带来的收益,页面的 Icons 组件 stat size 由 74KB 降到了 20KB,体积减小了 70%
观察页面,能够看到”课程大纲“、”课程详情“、”购课须知“这三个模块并不在页面的首屏渲染内容里,
咱们能够考虑对页面这几部分组件进行拆分再延迟加载,减小业务代码JS大小和执行时长
拆分的方式不少,可使用react-loadable、@loadable/component 等库实现,也可使用React 官方提供的React.lazy
拆分后的代码
代码拆分会致使组件会有渲染的延迟,因此在项目中使用应该综合用户体验和性能再作决定,经过拆分也能使部分资源延后加载优化加载时间。
项目中使用了 TreeShaking的优化,用时候要注意 sideEffects 的使用场景,以避免打包产物和开发不一致。
通过上述优化步骤,总体打包内容:
JS 文件数量6个,整体积 308KB,最大文件体积 109KB
关键 JS 优化数据对比:
文件整体积 | 最大文件体积 | |
---|---|---|
优化前 | 460.8 kb | 170 kb |
优化后 | 308 kb | 109 kb |
优化效果 | 整体积减小 50% | 最大文件体积减小 56% |
页面中包含了一些上报相关的 JS 如 sentry,beacon(灯塔 SDK)等,对于这类资源,若是在弱网状况,可能会成为影响 DOM 解析的因素
为了减小这类非关键JS的影响,能够在页面完成加载后再加载非关键JS,如sentry官方也提供了延迟加载的方案
在项目中还发现了一部分非关键JS,如验证码组件,为了在下一个页面中能利用缓存尽快加载,因此在上一个页面提早加载一次生成缓存
若是不访问下一个页面,能够认为这是一次无效加载,这类的提早缓存方案反而会影响到页面性能。
针对这里资源,咱们可使用 Resource Hints,针对资源作 Prefetch 处理
检测浏览器是否支持 prefech,支持的状况下咱们能够建立 Prefetch 连接,不支持就使用旧逻辑直接加载,这样能更大程度保证页面性能,为下一个页面提供提早加载的支持。
const isPrefetchSupported = () => {
const link = document.createElement('link');
const { relList } = link;
if (!relList || !relList.supports) {
return false;
}
return relList.supports('prefetch');
};
const prefetch = () => {
const isPrefetchSupport = isPrefetchSupported();
if (isPrefetchSupport) {
const link = document.createElement('link');
link.rel = 'prefetch';
link.as = type;
link.href = url;
document.head.appendChild(link);
} else if (type === 'script') {
// load script
}
};
复制代码
优化效果:非关键JS不影响页面加载
能够观察到onload被大量的图片资源和视频资源阻塞了,可是页面上并无展现对应的图片或视频,这部份内容应该进行懒加载处理。
处理方式主要是要控制好图片懒加载的逻辑(如 onload 后再加载),能够借助各种 lazyload 的库去实现。 H5项目用的是位置检测(getBoundingClientRect )图片到达页面可视区域再展现。
但要注意懒加载不能阻塞业务的正常展现,应该作好超时处理、重试等兜底措施
课程详情页 每张详情图的宽为 1715px,以6s为基准(375px)已是 4x图了,大图片在弱网状况下会影响页面加载和渲染速度
使用CDN 图床尺寸大小压缩功能,根据不一样的设备渲染不一样大小的图片调整图片格式,根据网络状况,渲染不一样清晰度的图
能够看到在弱网(移动3G网络)的状况下,同一张图片不一样尺寸加载速度最高和最低相差接近6倍,给用户的体验大相径庭
CDN配合业务具体实现:使用 img 标签 srcset/sizes 属性和 picutre 标签实现响应式图片,具体可参考文档
使用URL动态拼接方式构造url请求,根据机型宽度和网络状况,判断当前图片宽度倍数进行调整(如iphone 1x,ipad 2x,弱网0.5x)
优化效果:移动端 正常网络状况下图片体积减少 220%、弱网状况下图片体积减少 13倍
注意实际业务中须要视觉同窗参与,评估图片的清晰度是否符合视觉标准,避免反向优化!
iframe
加载 iframe 有可能会对页面的加载产生严重的影响,在 onload 以前加载会阻塞 onload 事件触发,从而阻塞 loading,可是还存在另外一个问题
以下图所示,页面在已经 onload 的状况下触发 iframe 的加载,进度条仍然在不停的转动,直到 iframe 的内容加载完成。
能够将iframe的时机放在 onload 以后,并使用setTimeout触发异步加载iframe,可避免iframe带来的loading影响
数据上报
项目中使用 image 的数据上报请求,在正常网络状况下可能感觉不到对页面性能的影响
但在一些特殊状况,如其中一个图片请求的耗时特别长就会阻塞页面 onload 事件的触发,延长 loading 时间
解决上报对性能的影响问题有如下方案
H5项目采用了延迟合并上报的方案,业务可根据实际须要进行选择
优化效果:所有数据上报在onload后处理,避免对性能产生影响。
字体优化
项目中可能会包含不少视觉指定渲染的字体,当字体文件比较大的时候,也会影响到页面的加载和渲染,可使用 fontmin 将字体资源进行压缩,生成精简版的字体文件、
优化前:20kB => 优化后:14kB
目前咱们在STKE部署了直出服务,经过监控发现直出平均耗时在 300+ms
TTFB时间在 100 ~ 200 之间波动,影响了直出页面的渲染
经过日志打点、查看 Nginx Accesslog 日志、网关监控耗时,得出如下数据(如图)
登录 NGW 所在机器,ping STKE机器,有如下数据
平均时延在 32ms,tcp 三次握手+返回数据(最后一次 ack 时发送数据)= 2个 rtt,约 64ms,和日志记录的数据一致
查看 NGW 机器所在区域为天津,STKE 机器所在区域为南京,能够初步判断是由机房物理距离致使的网络时延,以下图所示
切换NGW到南京机器 ping STKE南京的机器,有如下数据:
同区域机器 ping 的网络时延只有 0.x毫秒,以下图所示:
综合上述分析,直出页面TTFB时间过长的根本缘由是:NGW 网关部署和 Nginx、STKE 不在同一区域,致使网络时延的产生
解决方案是让网关和直出服务机房部署在同一区域,执行了如下操做:
优化前
优化后
优化效果如上图:
七天网关平均耗时 | |
---|---|
优化前 | 153 ms |
优化后 | 31 ms 优化 80%(120 ms) |
模拟弱网状况(slow 3g)Performance 录制页面渲染状况,从下图Screenshot中能够发现
CSS不会阻塞页面解析,但会阻塞页面渲染,若是CSS文件较大或弱网状况,会影响到页面渲染时间,影响用户体验。
借助 ChromeDevTool 的 Coverage 工具(More Tools里面),录制页面渲染时CSS的使用率
发现首屏的CSS使用率才15%,能够考虑对页面首屏的关键CSS进行内联,让页面渲染不被CSS阻塞,再把完整CSS加载进来
实现Critial CSS 的优化能够考虑使用 critters
优化后效果:
CSS 资源正在下载时,页面已经能正常渲染显示了,对比优化前,渲染时间上 提高了 1~2 个 css 文件加载的时间。
观察页面的元素变化
优化前(左图):图标缺失、背景图缺失、字体大小改变致使页面抖动、出现非预期页面元素致使页面抖动
优化后:内容相对固定, 页面元素出现无突兀感
主要优化内容:
优化效果由如下指标量化
首次内容绘制时间FCP(First Contentful Paint):标记浏览器渲染来自 DOM 第一位内容的时间点
视窗最大内容渲染时间LCP(Largest Contentful Paint):表明页面可视区域接近完整渲染
加载进度条时间:浏览器 onload 事件触发时间,触发后导航栏进度条显示完成
Chrome 模拟器 4G 无缓存对比(左优化前、右优化后)
首屏最大内容绘制时间 | 进度条加载(onload)时间 | |
---|---|---|
优化前 | 1067 ms | 6.18s |
优化后 | 31 ms 优化 80%(120 ms) | 1.19s 优化 81% |
Lighthouse 跑分对比
优化前
优化后
性能得分 | |
---|---|
优化前 | 平均 40 ~ 50 |
优化后 | 平均 75 ~ 85 提高 47% |
srobot 性能检测一周数据
srobot 是团队内的性能检测工具,使用TRobot指令一键建立页面健康检测,定时自动化检测页面性能及异常
优化前
优化后
进度条平均加载(onload)时间(4G) | |
---|---|
优化前 | 4632ms |
优化后 | 2581ms 提高45% |
感谢耐心阅读,欢迎你们交流,指正文中错误和疏漏,一块儿学习!