NickHoo
Published on

30分 到 90分, 老网站的一次回春手术 🌱

Authors
  • avatar
    Name
    NickHoo
    Twitter
poster

常有用户反馈网站速度太慢,甚至谷歌排名也受到影响。

作为开发的我们也苦其久矣……

终于有机会优化一下了 🚀

开工之前,不禁要问:

  1. 具体慢在哪里?
  2. 可以怎么优化?

接下来就让我们一步步寻找答案。

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
https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/gMp7ldy8b7PonBQN/img/dfa9d71d-9f03-46d2-9455-29b25507335d.png

虽然事前知道比较差,没想到这么差,尤其这冗余代码量就离谱。

吐槽结束,接下来,让我们针对测试结果中的各项指标做一下具体分析。

1.4 分析原因

上述测试结果,各指标的分数看作是宏观结果的话,那原因就藏在 树状图、网络瀑布流、性能火焰图等细节。

让我们先找到所有病灶,逐个加入问题清单,并记录初步对策,最后来统一制定治疗方案。

编号问题对策
???

1.4.1 树状图 找出最短的短板

首先拿最离谱的冗余代码开刀,在lighthouse测试结果页,下方查看树状图。从下图可以看到最大冗余的是funcData.js​​。

https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/gMp7ldy8b7PonBQN/img/78a9eb1f-d6c7-4b96-9973-dd87c8c8ccac.png

这是个上万行的公共方法库,而本页只用到了其中了少数几个方法。

对策自然也就有了 —— 拆分模块、按需导入。

编号问题对策
1代码冗余拆分模块、按需导入

1.4.2 网络瀑布流 找出下载阶段的问题

从下图的瀑布流中看到,首先加载的是一大堆第三方依赖,主js排在挺靠后的位置。

追溯这些依赖 对应的功能 和 使用的时机,发现两个问题:

  1. 有的并不需要立刻加载,比如moxie​​ qiniu​​ handsontable​​ swiper​​ ​laydate​等,在用户有交互后才需要加载;
  2. 有的依赖过重 ,比如 jq-ui​ 仅仅为了实现 tooltip 而全量引入了整个ui库。

此外,代码都未经过压缩,cdn端也没有开启 http2,影响了并发下载速度。

https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/gMp7ldy8b7PonBQN/img/407b5e12-86ca-4f2a-8b2e-33e5c8c7ad70.png

编号问题对策
1代码冗余拆分模块、按需导入
2依赖过多精简、替换 依赖
3过早加载动态导入
4体积待优化压缩混淆
5http1开启http2

1.4.3 性能火焰图 找出执行阶段的问题

从性能火焰图中我们可以看到,主线程上有大量左上角带红色角标的执行任务,开发工具已经帮我们标注出了执行漫长的“长任务”。其中,大部分与上一步我们在网络瀑布流中发现的第三方依赖多有重合。真是 下载的慢,执行的久,把主线程堵的死死的。

同样是下图,选中的这个长任务,来源于 页面最底部的模块,无交互时既不需要下载也不需要执行。

以此类推,除第一屏外的部分页面、各类弹窗内部的功能 都可以在需要的时候再下载,并动态导入执行。

https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/gMp7ldy8b7PonBQN/img/cfea966a-1664-49ce-b53a-67ccd18238b2.png

编号问题对策
1代码冗余拆分模块、按需导入
2依赖过多精简、替换 依赖
3过早加载动态导入
4体积待优化压缩混淆
5http1开启http2
6第一屏外的部分页面、各类弹窗内部的功能动态导入

2. 优化

综合上方我们累计的问题清单,模块化、按需导入、动态导入、压缩混淆 这些概念甚是眼熟,都是现代前端框架必备的特性了。

没错,我们这网站的技术实在太古老了—— JSP+JQ。而这祖传的代码,随着一代代人的浇灌,一路野蛮生长。如今就有了我们面对的现状,总结其实就两点:技术落后、工程混乱。

2.1 制定方案

抱怨不是我们的目的,让我们继续思考如何对症下药:

  1. 技术落后,引入现代前端工具链可以解决一部分,这也是下一步解决工程混乱的基石;
  2. 工程混乱,需要结合业务重构代码结构,而这必须对当前业务功能有足够的熟悉。

最理想的是 直接上Next、Nuxt等方案,但是最复杂的用户身份体系、权限体系都藏在Java+JSP的犄角旮旯里,时间有限、开发资源有限,我们此时需要的是这样的方案:

  • 承接历史代码,面向未来开发;
  • 兼顾 SEO 和 开发体验;

经多一番对比和取舍,我们选择了一个过渡方案:轻量现代化的vite + 虽老但SEO友好的JSP:

  • vite轻巧,不依赖框架,方便后续迁移;快速,极大改善开发体验。

  • JSP对于前端同学虽然有诸多不便,但是暂时将大部分后端变更隔离出此次优化,后续实践也证明确实节约了不少精力。

让我们先迈出第一步:把现代化前端工具链引进来吧!

2.2 引入打包工具

第一期,我们先找个软柿子捏捏,在业务最简单的首页,尝试引入当红的打包工具 vite,验证一下思路。

为了兼容 新旧 两套代码,我们得重新设计一下前端项目工程:

image

此外,还需要自定义NodeJS脚本链接前端vite项目和JSP项目中的静态资源引用,包括开发阶段的代理,线上环境的动态URL,以及cdn加速等。

经过反复尝试,终于把 Vite 引入了 JSP 项目,过程太过曲折,以后有机会单独回顾一下。

第一期上线后实测,性能有了15分左右的提升,代码体积瘦身一半。

类别beforeafter
未达标数 / 指标总数3 / 63 / 6
性能评分中位数73分88分
冗余代码体积 / 代码总体积1.8M / 3M816kK / 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预防

  1. 及时给出 loading 反馈,建议优先使用现有的 loading 动画 和 光标loading 提示;
  2. 如果点击后动态导入速度慢,可以在鼠标悬停后做预加载 prefetch;
  3. 数据回显与动态加载需要兼顾;
  4. 避免重复渲染。

2.4 结果对比

开发、测试结束,第二期优化终于上线,优化前后的性能评分终于由红转绿:

类别beforeafter
未达标数 / 指标总数3 / 60 / 6
性能评分中位数39分94分
冗余代码体积 / 代码总体积4.7M/6.9M1014K / 1.6M
beforeafter
https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/gMp7ldy8b7PonBQN/img/dfa9d71d-9f03-46d2-9455-29b25507335d.pnghttps://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/gMp7ldy8b7PonBQN/img/c54e1410-551a-42f7-b64e-772243e97633.png
https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/gMp7ldy8b7PonBQN/img/78a9eb1f-d6c7-4b96-9973-dd87c8c8ccac.pnghttps://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/gMp7ldy8b7PonBQN/img/e7e2b2b8-ff0c-4fce-a4b9-55ff299c448b.png

其中,冗余代码主要谷歌分析的第三方依赖,不仅影响速度,而且还有被浏览器插件拦截屏蔽的问题,后续我们探索一下更优雅数据埋点分析方案。

3. 总结

至此,这场为老网站做的回春手术阶段性成功了🎉

poster
事后诸葛不禁提问:这轮优化结束了,如何避免重蹈覆辙呢……
  • YES or NO ?

    • 代码洁癖 ?

    • 规则约束 ?

      • code lint ?
      • git lint ?
    • 流程 ?

      • 排期 ?
      • 设计评审 ?
      • 代码评审 ?