- Published on
30分 到 90分, 老网站的一次回春手术 🌱
- Authors
- Name
- NickHoo
常有用户反馈网站速度太慢,甚至谷歌排名也受到影响。
作为开发的我们也苦其久矣……
终于有机会优化一下了 🚀
开工之前,不禁要问:
- 具体慢在哪里?
- 可以怎么优化?
接下来就让我们一步步寻找答案。
- 1.分析
- 1.1 选择工具
- 1.2 设计方法
- 1.3 测试结果
- 1.4 分析原因
- 1.4.1 树状图 找出最短的短板
- 1.4.2 网络瀑布流 找出下载阶段的问题
- 1.4.3 性能火焰图 找出执行阶段的问题
- 2. 优化
- 2.1 制定方案
- 2.2 引入打包工具
- 2.3 重构业务代码
- 2.3.1 技术要点
- 2.3.2 体验优化和bug预防
- 2.4 结果对比
- 3. 总结
1.分析
具体慢在哪里?我们需要用量化的指标来分析网站完整的工作流程。
tips: web性能量化指标 web.dev 有详尽的文档可供参考。
1.1 选择工具
市面上网站测速、性能分析的工具很多,如何选择工具呢?
既然影响的是谷歌排名,那就优先用谷歌的标准来测量、分析,经过试用、对比,果然谷歌的PageSpeed Insights + LightHouse目前应该是最优解:
- 现场即时分析无需排队等待
- 真实访客最近一个月的性能统计
- 测试结果可下载存档、可回放
- 代码冗余量分析
- 免费
但是谷歌的工具局限于不方便直观测试不同地区的性能,当然搭配网络魔法也可以解决,只是会多一层网络消耗,所以可以再用其他支持多地区测试的在线工具辅助。
1.2 设计方法
工具选定,再设计一下测试方法:
- 挑选出最重要的目标页面:首页、业务入口页;
- 无痕模式+相同网络环境 至少测3次;
- 开发环境和测试环境 lighthouse限速,模拟出相似的分数,便于开发阶段的验证;
- 测出上线前的各主要地区的指标,用于上线后的验证。
1.3 测试结果
3项核心指标 + 3项重要指标,共6项,红色为欠佳未达标:
首页:
3项未达标
性能评分中位数 73分
冗余代码体积/代码总体积:1.8M/3M
业务入口页:
- 3项未达标
- 性能评分中位数 39分
- 冗余代码体积/代码总体积:4.7M/6.9M
虽然事前知道比较差,没想到这么差,尤其这冗余代码量就离谱。
吐槽结束,接下来,让我们针对测试结果中的各项指标做一下具体分析。
1.4 分析原因
上述测试结果,各指标的分数看作是宏观结果的话,那原因就藏在 树状图、网络瀑布流、性能火焰图等细节。
让我们先找到所有病灶,逐个加入问题清单,并记录初步对策,最后来统一制定治疗方案。
编号 | 问题 | 对策 |
---|---|---|
? | ? | ? |
1.4.1 树状图 找出最短的短板
首先拿最离谱的冗余代码开刀,在lighthouse测试结果页,下方查看树状图。从下图可以看到最大冗余的是funcData.js
。
这是个上万行的公共方法库,而本页只用到了其中了少数几个方法。
对策自然也就有了 —— 拆分模块、按需导入。
编号 | 问题 | 对策 |
---|---|---|
1 | 代码冗余 | 拆分模块、按需导入 |
1.4.2 网络瀑布流 找出下载阶段的问题
从下图的瀑布流中看到,首先加载的是一大堆第三方依赖,主js排在挺靠后的位置。
追溯这些依赖 对应的功能 和 使用的时机,发现两个问题:
- 有的并不需要立刻加载,比如
moxie
qiniu
handsontable
swiper
laydate
等,在用户有交互后才需要加载; - 有的依赖过重 ,比如
jq-ui
仅仅为了实现 tooltip 而全量引入了整个ui库。
此外,代码都未经过压缩,cdn端也没有开启 http2,影响了并发下载速度。
编号 | 问题 | 对策 |
---|---|---|
1 | 代码冗余 | 拆分模块、按需导入 |
2 | 依赖过多 | 精简、替换 依赖 |
3 | 过早加载 | 动态导入 |
4 | 体积待优化 | 压缩混淆 |
5 | http1 | 开启http2 |
1.4.3 性能火焰图 找出执行阶段的问题
从性能火焰图中我们可以看到,主线程上有大量左上角带红色角标的执行任务,开发工具已经帮我们标注出了执行漫长的“长任务”。其中,大部分与上一步我们在网络瀑布流中发现的第三方依赖多有重合。真是 下载的慢,执行的久,把主线程堵的死死的。
同样是下图,选中的这个长任务,来源于 页面最底部的模块,无交互时既不需要下载也不需要执行。
以此类推,除第一屏外的部分页面、各类弹窗内部的功能 都可以在需要的时候再下载,并动态导入执行。
编号 | 问题 | 对策 |
---|---|---|
1 | 代码冗余 | 拆分模块、按需导入 |
2 | 依赖过多 | 精简、替换 依赖 |
3 | 过早加载 | 动态导入 |
4 | 体积待优化 | 压缩混淆 |
5 | http1 | 开启http2 |
6 | 第一屏外的部分页面、各类弹窗内部的功能 | 动态导入 |
2. 优化
综合上方我们累计的问题清单,模块化、按需导入、动态导入、压缩混淆 这些概念甚是眼熟,都是现代前端框架必备的特性了。
没错,我们这网站的技术实在太古老了—— JSP+JQ。而这祖传的代码,随着一代代人的浇灌,一路野蛮生长。如今就有了我们面对的现状,总结其实就两点:技术落后、工程混乱。
2.1 制定方案
抱怨不是我们的目的,让我们继续思考如何对症下药:
- 技术落后,引入现代前端工具链可以解决一部分,这也是下一步解决工程混乱的基石;
- 工程混乱,需要结合业务重构代码结构,而这必须对当前业务功能有足够的熟悉。
最理想的是 直接上Next、Nuxt等方案,但是最复杂的用户身份体系、权限体系都藏在Java+JSP的犄角旮旯里,时间有限、开发资源有限,我们此时需要的是这样的方案:
- 承接历史代码,面向未来开发;
- 兼顾 SEO 和 开发体验;
经多一番对比和取舍,我们选择了一个过渡方案:轻量现代化的vite + 虽老但SEO友好的JSP:
vite轻巧,不依赖框架,方便后续迁移;快速,极大改善开发体验。
JSP对于前端同学虽然有诸多不便,但是暂时将大部分后端变更隔离出此次优化,后续实践也证明确实节约了不少精力。
让我们先迈出第一步:把现代化前端工具链引进来吧!
2.2 引入打包工具
第一期,我们先找个软柿子捏捏,在业务最简单的首页,尝试引入当红的打包工具 vite,验证一下思路。
为了兼容 新旧 两套代码,我们得重新设计一下前端项目工程:
此外,还需要自定义NodeJS脚本链接前端vite项目和JSP项目中的静态资源引用,包括开发阶段的代理,线上环境的动态URL,以及cdn加速等。
经过反复尝试,终于把 Vite 引入了 JSP 项目,过程太过曲折,以后有机会单独回顾一下。
第一期上线后实测,性能有了15分左右的提升,代码体积瘦身一半。
类别 | before | after |
---|---|---|
未达标数 / 指标总数 | 3 / 6 | 3 / 6 |
性能评分中位数 | 73分 | 88分 |
冗余代码体积 / 代码总体积 | 1.8M / 3M | 816kK / 1.5M |
从上表可以看到优化有了明显的效果,验证可行。但这距离健康状态还差不少,接下来,让我们来点更大的改造。
2.3 重构业务代码
得益于第一期我们已经引入了 Vite,npm丰富的现代库 足以让我们替换掉jq-ui等笨重的传统库,加上 按需导入 和 动态导入 的支持,我们终于可以顺畅地使用 promise async/await 等现代原生es语法 来大胆重构业务逻辑。
我们把这个艰巨的任务结构化:
梳理现有的业务逻辑
- 上下游
- 前后端
重新设计
拆分/复用模块
- 细拆公共模块;
- 粗拆特有业务逻辑,包装为独立模块后复用;
设计动态加载
- 弹窗:打开时才加载;
- 按钮:出现在视野、悬停、点击 后才加载后置流程;
- 二屏:即将出现在视野 才加载;
挑选库
- xy-ui 作为基础UI库
- tip 替代 jq-ui
- 原生fetch 替代 jq-ajax
- 自封装上传组件 替代 qiniu-sdk
- 无法替代的 es5 库 , 包装一层 module loader
2.3.1 技术要点
此次重构的关键技术是动态加载,用到某个功能时才去下载对应的代码,效果如下:
实现方式大约是这几种:目标元素出现在视野、悬停、点击 等事件触发 import("moduleX.js")
,示例代码如下:
// 定义被观察者
const target = document.querySelector("XXX");
// 定义观察者
const Observer = new IntersectionObserver(async (entries, observer) => {
// 当目标元素 出现在视野内 且 未初始化
if (
entries[0].intersectionRatio > 0 &&
$(entries[0].target).attr("init") !== "true"
) {
$(entries[0].target).attr("init", "true");
// 动态加载 moduleX
const {default:moduleX} = await import("moduleX.js");
// 使用 moduleX
}
});
// 开始观察目标
Observer.observe(target);
2.3.2 体验优化和bug预防
- 及时给出 loading 反馈,建议优先使用现有的 loading 动画 和 光标loading 提示;
- 如果点击后动态导入速度慢,可以在鼠标悬停后做预加载 prefetch;
- 数据回显与动态加载需要兼顾;
- 避免重复渲染。
2.4 结果对比
开发、测试结束,第二期优化终于上线,优化前后的性能评分终于由红转绿:
类别 | before | after |
---|---|---|
未达标数 / 指标总数 | 3 / 6 | 0 / 6 |
性能评分中位数 | 39分 | 94分 |
冗余代码体积 / 代码总体积 | 4.7M/6.9M | 1014K / 1.6M |
before | after |
---|---|
| |
| |
其中,冗余代码主要谷歌分析的第三方依赖,不仅影响速度,而且还有被浏览器插件拦截屏蔽的问题,后续我们探索一下更优雅数据埋点分析方案。
3. 总结
至此,这场为老网站做的回春手术阶段性成功了🎉
事后诸葛不禁提问:这轮优化结束了,如何避免重蹈覆辙呢……
YES or NO ?
代码洁癖 ?
规则约束 ?
- code lint ?
- git lint ?
流程 ?
- 排期 ?
- 设计评审 ?
- 代码评审 ?