HTTP/2 已经不是新鲜玩意了,而本站之前一直是 HTTP/1.1,是因为我忘了/没注意到这个问题。刚才去升级了下,很简单,只要在 Ngixn 的网站配置文件里加上 http2 关键字,然后重启服务就行了。
试了下页面加载(展现)速度确实有了比较明显的提升。
具体提升了多少呢?我在某个页面进行测试(不使用缓存),下面是 1.1 和 2 的对比,可以稳定复现:
加载时间从 3.3 秒减少到 2.2 秒,完成用时从 3.6 秒减少到 2.5 秒,都减少了 1 秒钟。
从瀑布图可以看出,Chrome 浏览器对 1.1 协议的同源请求最多为 6 个,而 H2 是 11 个,这大大提高了并发请求数量。
而且 1.1 的许多请求前面都有个灰色的条,而且还比较长,但在 H2 中就很少。这部分是排队等待时间,即浏览器知道要从服务器下载这些文件,但因为前面说过的同时请求数量限制,所以处在后面的文件就需要排队等待。在 H2 中的等待时间大大减少了,而且单个请求的耗时也更少。
在另一个网页的 Lighthouse 测试中,对比如下:
前两项指标是模拟用户感知到某些内容的时间,这两项只要超过 1 秒就是黄色的(中等评价)。
FCP 绘制首个内容的时间从 1.4 秒缩短到 0.8 秒,减少了 0.6 秒(有时候减少的没有这么多)。对本站来说,这个“首个内容”就是网页里被直接解析和显示出来的文字内容。这部分内容只需要等待最前面的 12 个请求完成就可以显示,所以减少的时间相对较少。
LCP 绘制最大图片的时间从 2.2 秒缩减到 1 秒,足足减少了 1.2 秒,可谓提升巨大啊。这是因为文章主图的请求很靠后,在第 30 多个请求。请求数量越多、越靠后,H2 对比 1.1 的优势就越大。
不过从这项指标可以看出,本站的文件加载顺序存在巨大的问题,文章图片应该有较高的优先级,但实际上,伪春菜和一些小按钮等不紧要的图片资源都排在文章图片之前。而且如果文章有评论的话,评论者的头像图片也是先于文章图片进行加载的,这会导致文章首图的展示时间又被推迟。
对此我也很无奈,因为博客系统(WordPress)和主题、插件等都不是我能完全控制的。比如我怀疑某些由 WordPress 所添加的 css、js 资源可能是不必要的。小按钮、评论头像先加载这种事主题要背锅。伪春菜(网页右下角的小 saber)是插件控制的,它的资源插入的位置是在网页的 head 标签里,我感觉匪夷所思啊,它的 js 文件丢到 body 后面再加个 async 标记都是完全可以的。但是它的代码插入位置是伪春菜插件控制的(通过 wp_header 之类的钩子),想改的话除非从插件源码里找到相关代码修改。不巧我并不懂 PHP,所以不好修改。
评论区小图片懒加载
不过我还是想折腾下,先试试优化评论区图片的加载。我早就看评论区不爽了,因为我之前在评论区加 Emoji 表情的时候就在主题里找过相关源码,但当时没搞清楚。现在就再来搞搞它,这次终于搞清楚了。
我想给评论区的头像和浏览器、平台这三个图片加上懒加载效果。
那么首先要找到它们的 PHP 源码。这是在主题代码里的。不过它不在我现在用的子主题上,而是在父主题上。而且虽然父主题目录里有个 comment.php,但那里面只有外层的容器(html 代码),没有内部具体内容的代码。
里面内容的代码在父主题的 functions.php 里,主题作者写了个函数来输出详细内容:
头像内容是 get_avatar 函数产生的,那么这个函数在哪里呢?没找到,谷歌了一下发现这是 WordPress 内置的钩子。在主题代码里搜索 get_avatar,找到了使用这个钩子的地方,是在子主题的 functions.php 里。看来这几行代码是我以前从网上 copy 的,因为当时用的就是这个子主题,所以就加在子主题里了。
头像右侧的 UA 图标是个叫 clrs_wp_useragent 的自定义函数,添加图片的代码在一个 func/wp-useragent.php 里面,wtf
这些代码和函数散落在不同的文件里,有的甚至我都想不到。我是在服务器上用 grep -R 搜索关键词才找到的。
给这三个 img 标签加上 loading="lazy"
属性,然后刷新页面,终于有效果了,在显示首屏时,评论区的这些图片都没有被加载,减少了请求数量。
这是优化之前的,红框里是评论区的小图片:
至于文章里的首图,在上图里根本看不到,因为它在更下面的位置。
优化之后,浏览器没有加载评论区的小图片,首屏图片的加载位置提前了。
小知识:loading="lazy"
是浏览器原生支持的懒加载的标记。在这个案例中,当此页面显示首屏(网页顶部)时,由于评论区在网页底部,距离显示区域较远,所以浏览器不会加载这些图片。(但如果离显示区域比较近,比如距离首屏在 3- 4 屏以内,那么浏览器依然会加载)
减少 css 文件的数量
8 个 css 里,只有第一个和最后一个是主题里的。有 1 个是 WordPress 加载的,1 个是语法高亮的,剩下 4 个则来自 3 个不同的插件。
插件的先不去管他(有的是通过 wp_head 钩子搞的,我很头大),我这主题里为什么有两个 css 文件呢?我看了下,第一个是子主题的,然后子主题里引用了父主题的样式表。这样子主题里只需要写与父主题不同的那些样式就行。但是这样导致了 2 个请求,所以我现在直接把父主题的样式复制到子主题里,合并成一个,减少了一个请求,剩下 7 个。
接下来是 WordPress 自己插入的一个标记为 wp-block-library-css 的 css 文件,这个我在网上搜到了代码解决了。
<link rel='stylesheet' id='wp-block-library-css' href='https://saber.love/wp-includes/css/dist/block-library/style.min.css?ver=5.8.10' type='text/css' media='all' />
将下面代码添加到主题的 functions.php 里即可:
// 移除 wp-block-library-css 库加载的 css 文件
function adms_remove_wp_block_library_css(){
wp_dequeue_style( 'wp-block-library' );
wp_dequeue_style( 'wp-block-library-theme' );
wp_dequeue_style( 'wc-blocks-style' ); // Remove WooCommerce block CSS
}
add_action( 'wp_print_styles', 'adms_remove_wp_block_library_css', 100 );
这样又减少了一个 css 请求,剩下 6 个。
接下来让我无语的是 yet-another-related-posts-plugin 这个插件(就是文章底部的“相关文章”),它添加了 2 个 css 文件,每个只有 35 行,合计 70 行。
非要分成 2 个文件,好吧,按用途来看的话分成 2 个是合理的,但是你就这点内容至于吗?而且上图这个 css 文件在前台页面根本不需要啊,yarpp_help
、yarpp_help_msg
什么的,前台根本没有这样的元素啊。
在插件目录里查找到了添加这俩 css 文件的代码:
YARPP_Widget.php:10: wp_enqueue_style('yarppWidgetCss', YARPP_URL.'/style/widget.css');
YARPP_Core.php:1044: wp_enqueue_style('yarppRelatedCss', YARPP_URL.'/style/related.css');
把这俩都删掉,又少了俩 css 请求,剩下 4 个。
接下来是 /plugins/wp-mail-smtp/assets/css/admin-bar.min.css:
它的内容也很少,而且它负责的是管理界面顶部的设置入口。
由于前台页面我设置了不显示顶部的横条,所以它不应该在前台显示。我搜索得知可以用 is_admin()
判断是否处于后台管理页面,于是加了个判断条件,这样它就不会在前台页面里添加这个 css 了。
干掉这个之后,还剩下 3 个 css 文件。
接下来是 prism.css:
它是我手动插入的,是语法高亮的 JS 库配套用的样式表(虽然我觉得有点丑),由于其体积小,只有 2 KB,所以现在我把它合并到主题的 style.css 里了。这样又减少一个,剩下 2 个。
接下来是伪春菜的 /plugins/weichuncai/css/style.css,也只有 2 KB。它是由下面代码添加的:
sm-weichuncai.php:35: echo '<link rel="stylesheet" type="text/css" href="'.get_bloginfo('siteurl').'/wp-content/plugins/weichuncai/css/style.css">';
由于伪春菜我一直开着,所以我把这行代码注释了,然后把样式表的内容也并入主题的样式表里。
大功告成!这样 8 个样式表只剩下主题的 style.css 这一个了,减少了 7 个请求。
优化 js 文件
现在有 6 个 js 文件:
第一个是谷歌统计代码,不知道为什么最近都是加载失败,难道是我梯子屏蔽它了?先不管。
第二个是 JQuery,伪春菜依赖它,就先留着。
第三个是伪春菜的,先留着,但要调整下位置。
第四个是 WordPress 插入的,用于将嵌入的链接转换为其他形式。我不需要,去掉。
第五个是我的自定义代码,保留。
第六个是语法高亮,也保留。
先来干掉 WordPress 插入的 /wp-includes/js/wp-embed.min.js。经过搜索,将下面代码加入 functions.php 即可:
// 移除 /wp-includes/js/wp-embed.min.js
function my_deregister_scripts(){
wp_deregister_script( 'wp-embed' );
}
add_action( 'wp_footer', 'my_deregister_scripts' );
接下来是伪春菜的 common.js,它根本不需要放在 head 里,我要把它挪到底部去。它是在这里添加的:
sm-weichuncai.php:61: echo '<script src="'.get_bloginfo('siteurl').'/wp-content/plugins/weichuncai/js/common.js"></script>';
它位于一个叫 get_chuncai()
的函数里,并在 83 行添加到 wp_head 钩子。
我尝试将其改为 wp_footer 钩子,但是不符合我的要求,因为这样代码是输出在 <footer> </footer>
之间的,但我想将其放在 </body>
结束之后。我搜了一下, WordPress 好像没有这种钩子可以用。所以我将这一行注释掉,然后将其输出的内容(从网页源码里复制)直接粘贴到主题的 footer.php 里,放在 </body>
后面。
把它放在底部,一方面可以避免它在一开始就阻塞网页解析,另外它所引用的 3 个图片的加载实际也会延后。
但是这样操作之后,我发现正文首图的加载还是偏后,依然在伪春菜的图片之后。明明在源码里首图是在前面的,这是为什么呢?
其实查看源码不难发现,这是我使用的图片懒加载插件导致的。它会隐藏真实 img 标签,生成一个转圈圈的加载图标代替真正的图片。等到图片需要被显示的时候,再把转圈圈的图片换成真正的图片。
但问题在于,它把第一张图(首图)也给直接隐藏了!然后它会判断出来,哦,这个图片处于显示区域内,需要加载,于是再将其放出来,开始加载。但是此时已经晚了!所以之前首图的加载一直都在很靠后的位置。我真是服了。
文章源码里就是单纯的 img 标签,这个插件会处理成这样输出到前台:
这个插件仅作用于文章页内,就这它还把首图也默认隐藏,真是个天才。那么怎么让首图直接显示为原本的图片呢?如果从后端(这个插件源码)改的话,我估计他没写判断第一张图的逻辑,而我不懂 PHP,所以我也懒得写。我还是从前端解决吧。
我把下面的 js 代码放在文章区域后面,尽早把第一张图给恢复成原图。这样第一张图会尽早开始加载。
// 去掉文章里第一张图的懒加载,因为首图没有必要懒加载,
// 而且懒加载还会导致它加载靠后,发起请求晚,显示的也晚
const firstImage = document.querySelector('img.sl_lazyimg')
if (firstImage) {
// noscript 标签内部的 img 标签被视为纯文本,这里直接替换掉 noscript 元素
firstImage.nextSibling.outerHTML = firstImage.nextSibling.textContent
firstImage.remove()
}
(后来我从后端找到了不让第一张图被替换的方法,所以这个代码后来我又删掉了)
之后我把所有的 js 文件都改为异步加载了。
其中 JQuery 被其他代码依赖,必须同步加载,所以我将其改为内嵌在源代码里。
伪春菜的 js 文件之前也是同步的,而且在页面上还有一些零散的内嵌脚本,互相依赖。现在我把它的代码整合到一个文件里,这样它就可以变成异步的了。
js 文件都异步的好处是:
- 由于没有同步 js 文件的阻塞,现在 style.css 加载完之后就会立即触发 DOMContentLoaded 事件,比之前提前了 200 - 400 ms。这使得一些依赖 DOMContentLoaded 事件的代码能更早执行。
- 之前同步 js 加载完之后,js 代码才真正开始运行。现在这些代码不需要等待 js 加载完了,可以更早执行。
把小图片转为 base64 格式
有些背景图体积很小,但是却要单独请求,颇为浪费。
把它们转换成 base64 内嵌在 css 里可以节省请求次数。最下面俩是我新加的按钮,就是这么做的。至于单独的这三个是主题当初就是这样,现在我将其优化一下。
在开发者工具里查看图片,右键就可以将其复制位 URL 格式,也就是 base64 数据。
然后在样式表里替换掉原本的背景图片即可。
懒加载会产生 2 个小图片的请求,我也把它们转换成了 base64 内嵌文本。
不重要的图片可以设为 loading="lazy"
前面说过 loading="lazy"
可以让浏览器不加载位置比较靠下的、看不见的图片。但实际上即使对于能看到、需要加载的图片。 loading="lazy"
也是有用的,它可以降低图片的优先级,使其加载顺序变得靠后。
绿框处的图片加了 loading="lazy"
之后,加载顺序就跑到下面去了(我没有改变其在源代码中的位置)。
在没有加 loading="lazy"
的时候,图片是遇到就开始加载,并且其优先于 css 里的图片。但是加了 loading="lazy"
之后的图片加载顺序在 css 里的图片之后。当然也在正常图片之后。
之后我把 Logo 图也加了 loading="lazy"
。
红框里的是三个转换成 base64 的小图标,虽然还是显示在网络列表里,但是它是不需要走网络单独加载的。
另外我还去掉了一个已经失效的鼠标指针(左图有个损坏的图片的图标,就是个 ani 指针文件)。经过现在的测试,Chrome、Edge、Firefox 都不支持使用 ani 格式的自定义光标了。gif 格式可以用,不过只会显示第一帧,没有动态效果。
提高文章首图的加载顺序
其实本来不需要这个优化的,全怪懒加载插件,它搞出了负效果,我现在是在擦屁股。
这是某次加载的网络截图:
可以看出,Chrome 的资源加载顺序是:
- 文档本身
- 加载源码包含的同步文件,如 css、js、图片等(注意是同步的)
- 当 css 加载完成,并且内嵌的 js 代码执行完毕后,开始加载异步(
async
)的 js 文件,以及页面上新出现的图片(通过 js 生成的,插入到了页面上,也会被尽快加载)、以及背景图片、某些带有loading="lazy"
属性的图片 - 带有
loading="lazy"
属性的低优先级图片
我的目的是让文章首图尽早加载。现在首图(文件名开头是 202407 的)处于第 3 梯队,前面是同步的 css 和 js 文件。但是 logo 图片(head_0.jpg)却是在第 2 梯队的,它不需要等待 css 加载完。
为什么会这样呢?因为 logo 图是写在源码里的(img
标签),浏览器加载完文档后就会解析到它,马上开始加载。而首图并不是直接写在源码里的,懒加载插件在生成网页时就把首图处理成了一个占位用的图片,所以网页源代码里并没有首图,所以它也就不会出现在第 2 梯队。之后 js 代码把首图添加到了页面上,此时浏览器才知道要加载这样一个图片,它也就跑到了第 3 梯队。
那怎么让首图进入第 2 梯队呢?一个方法是让懒加载插件不要隐藏掉首图,让它直接出现在源码里。这需要修改插件的 PHP 代码,但是我不会。而且它也没有设置面板,不能设置“不要隐藏第一张图”的效果。
查看源码可以发现,它是匹配文章里的所有 img 标签,然后逐个将其转换成懒加载的代码。源码里没有判断“这是不是第一张图”的代码,不过我发现当原 img 标签里包含或缺少一些特定字符时,它是不会转换的。例如:
比如在第一行,如果它检测到图片是 gif 图,就不会转换。
再下面,如果图片没有 src 值(也就是空图片),也不会转换。
再往下,如果 img 标签里含有 skip_lazyload 标记,也不会转换。
那么办法就很简单了,只要给第一张图加上 skip_lazyload 属性即可,例如:
<img skip_lazyload src="/f/20240714_011912.jpg">
(顺便,我将这个标记改为了简短一些的 no_lazy)
我使用的是 Markdown 编辑器,所以图片是 Markdown 语法,编辑器在保存文章时会自动将图片转换成 img。
这样的话可以在图片描述里添加 no_lazy 标记,它会被转换成 img 的 alt 属性的一部分,同样可以达到目的。例如:
![2024年06月里番合集 no_lazy](/f/20240714_214332.jpg)
经过测试,确实有效:
首图现在和 css 是一同开始加载的,它不需要等待 css 先加载完成了。
还有另一个办法:在网页头部加入一行预加载代码,例如:
<link rel="preload" href="/f/20240714_011912.jpg" as="image" />
其实本质是一样的,就是让源码里出现这个图片。第一个方法更容易些,第二个则需要修改后台代码,自动提取文章里的第一张图片,为其生成这样的预加载代码(或者在正文里生成个 img 标签,都一样)。
要让图片处于第二梯队,必须让浏览器一开始接收到的源代码里就有这个图片。所以这必须在后台处理,前台代码(js)是无法达到这个效果的。我前面写了用 js 代码把被隐藏的首图显示出来,就是前台处理,所以会导致图片跑到第 3 梯队。现在改为在后台处理,就不需要那处 js 代码了。
现在让图片和 css 一起加载,不需要等 css 先加载完,这可以让图片更早的显示出来。
提一嘴,我之前以为图片的渲染会等待 DOMContentLoaded 完成,但实测发现不是,Chrome 会尽快让图片显示出来。所以对上面截图里的情况来说,首图不需要等待加载 css 的 500 毫秒,那么它就实打实的会早 500 毫秒显示出来。这个提升是巨大的。(虽然假如不用懒加载插件的话,根本不会有这个问题)
广告图的优化
两方面:
- 之前是用 js 加载广告图,然后添加到页面顶部。这会导致页面抖动,现在改为在源码里添加这个图片,避免了抖动。
- 使用 preload 技术,可以让浏览器尽快加载图片。前面已经说了,对于用 js 代码来动态加载的图片来说,preload 是个神技。但是我现在已经把广告图添加到源码里了,还需要预加载吗?
<link rel="preload" href="/f/广告图.png" as="image" />
答案是需要,因为不给它添加预加载的话,会导致文章首图的加载被延后,这个原理我也没搞懂。见文章末尾
优化背景图片的加载速度
之前背景图片显示出来的比较慢,这是因为它的加载时机比较晚。因为之前有同步加载的外部 js 文件,所以通过 js 来加载的背景图就必须等到同步 js 加载完才能执行。
而且背景图片有多个,随机使用一个,所以不适合使用预加载的方式来使加载时机提前(除非把所有背景图都放到预加载里,但这样体积会达到 10 MB)。
现在我把 js 文件都改成异步的了,又把背景图的 js 代码改为内嵌的,这样它只需要等待 css 加载完(DOMContentLoaded 之后)就会运行,这可以使加载背景图的时机提前 300 - 500 ms 左右。
红色竖线是 js 文件加载完的时间。之前背景图片在这时候才会开始加载,而现在提前了不少,因为现在背景图是跟在 css 文件后面了。
其他尝试
我试了把唯一的 css 文件改为内嵌的,不过这没有任何作用,我就取消了。
优化总结
某个文章页面从 45 个请求减少到了 25 个,并且其中有 10 个是内联资源,实际只有 15 个请求,是之前请求数量的三分之一。
比起以前的加载速度有了很大提升,这不仅是请求数量减少的作用,更重要的是大图片的加载时机真的提前了,而不是像之前那样简直浪费时间。首图、背景图、广告图的显示都至少提前了 500 ms。
之前首图加载顺序在第 40 位左右,在网络界面里都掉出第一页了(前面有图我就不重复发了)。现在加载顺序是非常靠前。
不过由于源代码体积增大(主要是把 JQuery 的源码放进去了),导致文档本体加载变慢了大约 200 ms。这个我很疑惑啊,源代码从 15 KB 增加到 55 KB,增加了 40 KB 而已,为什么它的下载时间会增加 200 ms 呢?
优化到这里几乎已经没法再提升了。虽然看着加载完还是需要 2 秒,但已经不是页面代码的内部原因,而是在于网络延迟,因为机房在新泽西州。图上瀑布图里绿色的部分就是等待时间,经常达到 500 ms(指的是一来一回的总延迟,还包含了服务器内部耗时,不过我不清楚服务器花费了多少时间)。如果把同一时间开始的请求算作一个批次,一共 3 批(不算 favicon.ico),假如等待时间可以减少 300 ms,那么总的请求时间就可以减少 1 秒了。
ps:我发现多数访客在文章页面里会多出 4 个请求,都是评论区底部验证码的,这一个插件加了 4 个请求,其中还有 3 个同步 js 文件,真的蛋疼。
未解之谜
有时候,同样的请求,而且请求数量也不多,但是文章首图有时却会出现排队时间(应该是浏览器内部调度时的排队):
上图中,首图是第二梯队加载的,符合预期。
但是在下图中,它的开始位置虽然是第二梯队,但是却被推迟了,其开始时间(绿条)是在 css 加载完之后,实际表现就是第三梯队。
那么这两次的区别在哪儿呢?在于第一张图里,我在源码 head 里面给广告图 yejiang.png 添加了预加载 preload。第二张图里我去掉了广告图的预加载,结果首图被推迟了。
我很费解啊,首图确实是可以在第二梯队执行的呀,而且浏览器的同时并发请求数量应该也足够用啊,这个排队我是真没搞懂。
解决中文 tag 分页链接的错误
点击文章标题下的标签就会跳转到标签页面,当标签是中文时(如“福利”),有个存在了好几年的问题,就是底部分页网址里的中文被重复编码了。
比如:
// 中文原文网址
https://saber.love/tag/福利/page/2
// 正常编码的网址
https://saber.love/tag/%e7%a6%8f%e5%88%a9/page/2
// 被重复编码的网址
https://saber.love/tag/%25e7%25a6%258f%25e5%2588%25a9/page/2
后台返回的网页源源代码里就是最后一种,这是中文被编码两次后的结果,正常只需要一次就行了,但不知为何这里被编码了两次,这导致这个网址是错误的,无法访问。
之前我没搞清楚原因,就在前台脚本里写了点 js 代码修复这种网址。但是对于爬虫来说,它们可能会从源码里获取到错误的网址,从而导致某些问题。
今天我怀疑这个问题已经造成了某些后果,所以决定修复它。这个导航部分是父主题的 functions.php 里的代码生成的,其中调用了 WordPress 内置的方法 paginate_links($pagination)
来生成分页的 html 代码。这个内置方法输出的直接就是双重编码的错误结果,我也不知道为什么。
我用 PHP 的 URL 解码方法处理了一下就恢复正常了:
echo urldecode(paginate_links($pagination));
但是还有另一个问题(与这个问题是一同出现的):处于第二页或后续分页时(也就是网址里已经含有了 /page/xx 的形式),那么分页导航里其他页面的链接会多一个斜线:
这也是错误的网站,无法访问(虽然实际上是会跳转回首页啦~)。还好简单替换就能解决,于是这行代码就变得很蛋疼:
echo str_ireplace("//page", "/page", urldecode(paginate_links($pagination)));
把本站 HTTP 协议升级到了 HTTP/2,以及减少请求数量的优化
-
Google Chrome 126Windows 雀氏快了点~
现在都是秒开了.. -
Google Chrome 126Windows 现在没梯子就进不来了。。。
文中有个地方:
由于 jQuery 被其他代码依赖,必须同步加载,但我的服务器延迟较高,所以我将 jQuery 内嵌在网页源代码里了,避免从网络加载它。
在使用 cloudflare 优化网站后,由于静态文件是走 cloudflare 的 CDN 的,JS 文件加载很快,就没必要把 jQuery 内嵌在网页源码里了。
而且把体积较大的 JS 文件内嵌在源码里也有弊端,比如 jQuery 的源码有 100 KB,加在源码里之后,经过 GZip 压缩后还是占据了 35 KB 的体积。并且可能是由于服务器压缩它的耗时比较长,所以导致加载网页源码(文档)的耗时也较长。
把它从源码里改为从外部加载后,网页源码(文档)的加载耗时有了明显降低,现在我测试的表现是从 800 ms 降低到了 560 ms。