mylibs

“我苦心锻炼了三年,我变秃了,也变强了。” —— 琦玉老师

0x00 大纲

0x01 前言

四个月前,我在《你是来找茬的吧?对自己的博客进行调优》一文中探讨了以博客的使用者而不是开发者身份去进行优化,究竟能做到何种程度的问题。当时以 Edge 浏览器的开发者工具里的 lighthouse 评分和加载时间作为基准,经过一系列的针对性优化调整,将博客首页的评分逼近到了“性能(99/100)”、“无障碍阅读(100/100)”的水平,但是当时有一个遗憾,就是没能完成双百,那么时隔多日,这次就要集中一点,登峰造极,完成双百挑战。

0x02 书接上回

上回说到,我们优化后的首页 lighthouse 评分如下:

old-main-page

那么性能部分扣掉的一分是在哪里扣掉的呢?点击“查看计算器”链接,里面会有详细的一个评分情况,如下图所示:

old-scores-detail

可以看到评分是相当苛刻的,而且五项标准的分数权重不一样,也即是说,只要你有任何一项有短板,就算其它分数再高,也没办法获得100的评分。可以看到这里主要的扣分项是 FCP 首次内容绘制时间和 LCP 最大内容绘制时间,至于原因,其实在上一篇文章的末尾有提到过,就是有些资源属于页面强制加载项,我们作为使用者是没有办法去裁剪和控制的。

既然堵不住,那能不能加速呢?

0x03 性能调优

DNS 预获取(DNS-prefetch)

在自己的博客首页,按 F12 打开开发者工具,切换到“网络”标签,然后刷新博客首页,在 URL 一列,可以查看到所有资源的加载的URL地址。然后你会发现,这些资源并非全部来源于同一个服务器,至少可以看到以下不同于主站地址 www.cnblogs.com 的二级或三级子域名:

https://common.cnblogs.com/
https://images.cnblogs.com/
https://pic.cnblogs.com/
https://blog-static.cnblogs.com/
https://account.cnblogs.com/

我们打开命令提示符或者其它你喜欢的终端,输入nslookup命令(注意不能用ping指令,部分服务器出于安全考虑,是禁用 ICMP 协议访问的),查看各个域名对应的 DNS 解析地址,就会发现它们的 IP 地址是不一样的,以主站和 pic.cnblogs.com 为例:

nslookup-result

那么如果将所有要访问的域名 DNS 提前进行解析,是不是可以加快访问速度呢?答案是肯定的。借助 DNS 预获取(DNS-prefetch)技术,可以达到我们的目的,在 MDN Web Docs 上,对于该技术是这样阐述的:

DNS-prefetch (DNS 预获取) 是尝试在请求资源之前解析域名。这可能是后面要加载的文件,也可能是用户尝试打开的链接目标。

当浏览器从(第三方)服务器请求资源时,必须先将该跨域域名解析为 IP 地址,然后浏览器才能发出请求。此过程称为 DNS 解析。DNS 缓存可以帮助减少此延迟,而 DNS 解析可以导致请求增加明显的延迟。对于打开了与许多第三方的连接的网站,此延迟可能会大大降低加载性能。

DNS-prefetch 可帮助开发人员掩盖 DNS 解析延迟。

以我自己的博客为例,我将以下域名加入了预取列表(注意因为这些地址都是基于 HTTPS 协议进行访问,所以我这里省略了协议名称,但如果你是加载第三方资源的,务必知晓其访问协议,可能需要指定 HTTP 前缀,请根据实际情况修改):

<link rel="dns-prefetch" href="//common.cnblogs.com">
<link rel="dns-prefetch" href="//images.cnblogs.com">
<link rel="dns-prefetch" href="//pic.cnblogs.com">
<link rel="dns-prefetch" href="//blog-static.cnblogs.com">
<link rel="dns-prefetch" href="//account.cnblogs.com">

注意这里不需要添加主站的地址,因为主站的 DNS 在浏览器访问那一刻已经被解析过了。

预连接(preconnect)

DNS 预获取(DNS-prefetch)技术可以与预连接(preconnect)技术联用,这里同样援引 MDC Web Docs 的解释:

DNS-prefetch 仅执行 DNS 查找,但preconnect会建立与服务器的连接。如果站点是通过 HTTPS (提供)服务的,则此过程包括 DNS 解析,建立 TCP 连接以及执行 TLS 握手。将两者结合起来可提供进一步减少跨域请求的感知延迟的机会。

在上面的 DNS 预获取列表后,增加下面的预连接列表:

<link rel="preconnect" href="//common.cnblogs.com">
<link rel="preconnect" href="//images.cnblogs.com/">
<link rel="preconnect" href="//pic.cnblogs.com/">
<link rel="preconnect" href="//blog-static.cnblogs.com/">
<link rel="preconnect" href="//account.cnblogs.com/">

我们看下效果,以userinfo接口调用为例(访问 account.cnblogs.com)为例,增加 DNS 预获取优化之前耗时是这样的:

dns-lookup-time-before

优化之后,变成了这样,可以看到还是有效果的:

dns-lookup-time-after

预加载(preload)

如果页面里面有大量链式加载的资源就要注意了,这往往意味着前置资源加载之前,用到了后续关键资源的地方就有可能被阻塞,理想情况下,所有资源的请求链应尽可能的短。但是对于没有办法避免链式加载,而所需的资源又确定会在后续渲染当中用到的场景,就可以用预加载(preload)技术来优化。

以自定义的头像为例,需要由自定义的主题脚本通过prepend的方式动态添加节点,这意味着主题脚本和jquery-2.2.0.min.js完成加载之前,该头像都暂时不可用。但是头像的 URL 地址我们是预先知道的,这个等待就白白浪费了许多时间。

request-chains-before

我们可以把即将要用到的有确定地址的资源告诉浏览器,让它把数据提前准备好,像这样:

<link rel="preload" as="image" href="//images.cnblogs.com/likecs_com/mylibs/1647185/o_200214034545avatar.png">

request-chains-after

可以看到请求链变短了,而且加载时间也提前了。同时也要注意,预加载(preload)并非在所有浏览器中都支持,不过好在它可以安全降级,顶多是不生效,不会影响到页面的正常使用。

compatibility-of-preload

减少不必要的 HTTP 调用

尽管我已经在博客园后台-选项页面中的“侧边栏控件”部分,取消勾选了“日历”模块,但不知为何网络请求中还是有一次日历接口的调用,尽管它啥也没干,却白白浪费了几十毫秒……

remove-useless-ajax-call-0

这个调用是从公告栏里面的loadBlogDefaultCalendar函数发起的,这个函数定义在blog-common.min.js文件里——上一篇文章提到过,这个文件是默认加载的,是博客园的公共JavaScript函数库,无法屏蔽。

remove-useless-ajax-call-1

但是loadBlogDefaultCalendar是作为全局函数发起匿名调用的,那么在blog-common.min.js文件加载之后,loadBlogDefaultCalendar函数执行之前,我们是可以利用JavaScript Function Hijacking把它替换掉的,像这样:

<script>window.loadBlogDefaultCalendar=Function.prototype</script>

它刚好可以与前面所有的优化一起放在博客园后台-设置页面中“页首 HTML 代码”处,保存后刷新页面,再看原来的 AJAX 请求已经没有了:

remove-useless-ajax-call-2

使用自定义的语法高亮

原先我使用的是园子自带的prism.js语法高亮,它会根据页面中的language type自动加载对应语言的高亮模块。对于一个页面有多种语言或开启了行号显示的情况,可能会发生多次的模块加载,那么你会在控制台看到prism-autoloader.min.js同时加载了若干个prism-*开头的脚本。那为什么说是可能呢?因为prism.js里面其实已经集成了多种常用语言,除非你用到了里面没有的语言模块或者插件,才会触发加载动作。

出于减少网络请求次数和缩减体积的目的,我决定使用自定义的语法高亮,在prism.js官网上可以很方便的定制模块,只勾选自己常用的几种语言即可,BTW,我还使用了彩虹括号插件……总之,像买菜一样选择自己想要的东西就好了:

custom-prism

最终你会得到一份 JavaScript 和一份 CSS 文件,把它像自定义脚本和主题一样引入到博客里面就可以了。如果你追求极限,可以将它们与你自己的脚本和样式文件合并——当然这是有代价的,不利于后期的独立维护和升级。

进一步精简 JavaScript 和 CSS

在开发者工具里找到”覆盖范围“标签,如果没有,可能要点击旁边的”+“号手工添加。点击”开始检测覆盖率并刷新页面“,覆盖率统计在你手工点击停止前会一直进行,这时候你可以去页面进行各种操作,尽可能地触发代码。随后在覆盖率报告中,可以看到当前各个 JavaScript 和 CSS 的使用情况:

coverage-01

点击具体的文件,会跳转到”源代码“标签。 蓝色表示该代码已被执行过,红色表示这一行代码未运行,在加载网页时不需要:

coverage-02

注意:仅仅依靠红色来判断代码未执行是不靠谱的,因为这并不表示该代码永远不会用到,这里仅仅是统计了页面加载时的覆盖率,如果你在页面执行一些其它的操作,很有可能就会触发更多的代码执行,覆盖率也会随之发生变化。

这样做的目的是找出优先加载和优先执行项,如果脚本和样式表体积较大,就可以按照执行优先级拆分,利用前面提到的预加载(preload)技术将最基础部分优先加载,后续使用到的脚本和样式延迟加载或者按需加载。尽量减少或加快关键资源的加载,依然是提高 FP、FCP 和 LCP 分数的关键。

性能辅助分析

如果说 lighthouse 能帮你评估页面的总体情况的话,那么性能分析工具则可以助你从细节入手找到瓶颈。同为开发者工具里,切换到“性能”标签,点击”开始分析并重新加载页面“按钮,能够自动刷新当前页面并对其进行采样分析,最终生成的报告如下所示:

performance

在这里可以看到浏览器在渲染当前页面时所做的各项工作的耗时统计以及负载分析,在报告摘要中,详细罗列了各个步骤所消耗的时间占比,建议从占比最大的部分开始优化,因为这样的收益可能是最高的。

如果性能报告中出现了红色三角形长任务(被标记红色),也是需要重点关注的,它指示主线程上耗时过长且性能缓慢的工作,通过查看对应时间轴火焰图上最宽的部分,找到耗时的原因。

此外,还可以关注 FP、FCP、LCP 的触发时机,以及 CLS 的触发次数和位置等诸多细节部分。前面我们提到过,如果 CLS 发生次数过多,将会使用户体验下降,同时严重影响评分。

另一类比较关键的事件是”重新计算样式“事件,如果发现了长时间运行的 “重新计算样式”事件,可以选中它,然后在下方点击 “选择器统计信息”功能来了解哪些 CSS 选择器占用的时间最多。从我的个人经验来看,通常来说 CSS 的优化收益不是很大(微秒级),除非有很严重的性能问题,选择前面几项耗时最突出的选择器进行优化是比较划算的方案。

coverage-03

由于每个使用者的页面情况不尽相同,只能针对性的进行分析,这里只能描述下大概的思路。

0x04 小结

在经过一系列的究极折磨后,这是首页最终的 lighthouse 评分:

new-main-page

new-scores-detail

可以看到,我们其实并没有拿到满分,只是近似满分:

(0.1×98 + 0.1×100 + 0.25×99 + 0.3×100 + 0.25×100)/(0.1 + 0.1 + 0.25 + 0.3 + 0.25) = 99.55

看来还可以继续寻找新的优化方法。

相关文章: