Back
Please upgrade your browser or check your network connection.

快 快 快 —— 如何拥有一个秒开的博客

Update

  • 21-05-09: 调整层级、在部分地方增加了更详细的说明以及增加了小章节 – 避免图片抖动而引发性能问题
  • 21-05-03: 优化了章节的阅读顺序。

TODO:

  • 本篇文章的DEMO

致谢

非常感谢苏卡卡大佬的下列文章:

没有大佬就没有我系列。

快吗?

这几天花了我挺多的时间去做了博客的优化。点进来博客的你,有觉得快吗?

Mobile 环境下的测试。

优化的方向

干什么事情都得有个方向,web优化也是不例外。实际上我本人对于这些技术都是一知半解,可能会出现很多知识漏洞。

要让一个网页快起来,需要在多个方向上面着手。每个部分都做到了,那么肯定就快起来了。

  • 服务器

  • CDN

  • 网络 - 在各个层面上都尽量减少RTT

  • 静态资源优化 - 压缩、减少体积(源码级别)

  • 优化关键渲染路径 - 保证首次加载只有HTML与CSS

  • 延迟非关键资源加载

  • 尽快触发FCP - 尽快显示页面,缓解用户焦虑

  • 优化长文章的渲染性能

  • 避免图片抖动而引发性能问题

  • 优化第三方重量JS库

  • 使用Service Worker 缓存 - 不仅仅局限于web服务器的Cache-Control字段

  • 网页内容预加载 - 让用户不再花时间在获取HTML上

服务器

Update: 21-09-08 本人的博客已经托管到 CloudBase Webify 。下面的很多东西都已经不符合现状啦。现在托管送300块的代金券。RMB真香。

我现有的博客分别托管在两个域名下面:

有群友问我,你的博客打开得这么快,服务器是不是比较贵的?

实际上,我现在放博客的服务器是一台腾讯云的学生机 1C1G1Mbps。可以访问试试速度 - https://zsnmwy.net/

而另外一个域名则是放在Cloudflare Workers Site上面。可以访问试试速度 - https://blog.zsnmwy.net/

我在手机网络(联通4G)下访问这两个站点,速度都觉得差不多。

所以我想说的是,对于轻量的静态博客来说,服务器的配置是没有那么重要的。

与其挑服务器,还不如花钱买一个好一点的网络线路。

CDN

  • HTML、CSS: 只有子域名blog.zsnmwy.net用上了CF的CDN,主域名zsnmwy.net是没有用任何的CDN加速。
  • 图片:用了Gitee以及jsDelivr
  • 第三方库:使用了jsDelivr 或编译进HTML

只要是需要从npm或者GitHub引入资源,我都是推荐使用jsDelivr。 jsDelivr在中国大陆是拥有CDN节点的,用来引入一些热门的小文件是特别推荐的。大文件以及冷门资源确实感觉不咋地。


如果是谷歌的官方服务器资源,务必找替代的源或者中转绕过。例如:

  • Google Analytics
  • Google Fonts
  • Google workbox

Google Analytics 可以使用Sukka大佬的方案,避免引入了45KB的JS脚本。还可以一定程度上规避禁止追踪的问题。

Google Fonts 还是直接把ttf文件下到本地,跟代码放在一块吧。因为真的没有什么好的替代方案。或许利用Cloudflare Worker写个中转。

Google workbox可以使用 npm包 - workbox-cdn,但是这个包的最新版5.1.4跟Google那边6.1.5差了一个大版本。其他的包用importScripts()真的不好通过URL导入。当然你也可以根据workbox-cdn的build.sh一个npm包或者自己提取出来。


如果你非常富,完全可以使用各种付费的CDN服务。

网络

  • Queueing

    The browser queues requests when:

    • There are higher priority requests.
    • There are already six TCP connections open for this origin, which is the limit. Applies to HTTP/1.0 and HTTP/1.1 only.
    • The browser is briefly allocating space in the disk cache
  • Stalled. The request could be stalled for any of the reasons described in Queueing.

  • DNS Lookup. The browser is resolving the request’s IP address.

  • Initial connection. The browser is establishing a connection, including TCP handshakes/retries and negotiating an SSL.

  • Proxy negotiation. The browser is negotiating the request with a proxy server.

  • Request sent. The request is being sent.

  • ServiceWorker Preparation. The browser is starting up the service worker.

  • Request to ServiceWorker. The request is being sent to the service worker.

  • Waiting (TTFB). The browser is waiting for the first byte of a response. TTFB stands for Time To First Byte. This timing includes 1 round trip of latency and the time the server took to prepare the response.

  • Content Download. The browser is receiving the response.

  • Receiving Push. The browser is receiving data for this response via HTTP/2 Server Push.

  • Reading Push. The browser is reading the local data previously received.

Source: https://developer.chrome.com/docs/devtools/network/reference/#timing-explanation

如果对下面的名词不太了解可以参考上面的引用喔~~


TCP通道的建立会花费大量的时间以及资源,而且每次round trip都会花费大量的时间,我们应该利用好每次成功建立的TCP通道以及round trip。

这里的round trip主要是指每次请求资源时候需要的TCP握手/重试时间以及服务器准备资源的时间。

实际上,这个叫 来回通信延迟, Round-trip delay time,RTT。

DNS

在DNS这一块,我觉得更重要的是在 服务商的SLA以及网络节点。但是我这里没有推荐。

但是你可以利用在后面提到的dns-prefetch来缓解DNS带来的体验问题。

SSL

在Chrome的定义中,Initial connection是已经包含了TCP handshakes/retries、negotiating an SSL。

SSL在Initial connection中占比96%,那也就是说,只要优化了此处,就等于赢得胜利。

这里贴一个相关的优化指南 - 八个HTTPS和SSL优化使用心得-减少等待时间和降低Https性能损耗

下面这两个一定要做,性能提升巨大:

  • HTTP/2
  • TLS1.3

SSL Configuration Generator 可以帮你直接生成适合使用的SSL配置。

TLS 1.3 - Enhanced Performance, Hardened Security TLS 1.2与1.3的对比。TLS 1.3 的RTT比TLS1.2 少了。

TTFB

Waiting (TTFB). The browser is waiting for the first byte of a response. TTFB stands for Time To First Byte. This timing includes 1 round trip of latency and the time the server took to prepare the response.

简单得讲,TTFB就是浏览器每次请求服务器资源时候的必要沟通时间。只要你请求一次,就肯定有一次TTFB时间。

通过Chrome Network,可以分别看到两个站点HTML文件的TTFB:

可以见到CF的TTFB基本上是广州腾讯云的两倍,可以间接说明海外的网络资源到我这边确实得花点时间。

广州腾讯云的Content Download是CF的3.18倍。

同样的内容,在更好的网络环境下,这两者的Content Download时间是反过来了,广州腾讯云的时间比CF的还长。

造成这个现象,是因为我在广州腾讯云上面启用了HTTP/2 Server Push,在获取HTML资源的时候,就将首页需要使用到的资源(css、img、js)推送到浏览器中。


Initiator中带Push的都是服务器进行主动推送。

图中蓝色块都是在接受或者读取Push。

通过观察Waterfall,可以看到加载主页面的HTML的时候就已经在接收其他资源的Push Data。那么只要设置恰当,就等同于使用一个TTFB的时间,就接收到了首页需要的所有资源。

那么我们再来看看不使用HTTP2 Server Push的效果,就知道差多远了。

每请求一次,就等同于多一个TTFB。

附上我的Caddy Web Server配置:

 1{
 2	servers :443 {
 3		protocol {
 4			experimental_http3
 5		}
 6	}
 7}
 8
 9www.zsnmwy.net {
10	redir https://zsnmwy.net{uri}
11	header {
12		Strict-Transport-Security max-age=31536000;
13		X-Content-Type-Options nosniff
14		X-Frame-Options DENY
15	}
16}
17
18zsnmwy.net {
19	root * /root/blogWeb
20	encode zstd gzip
21	file_server
22	header {
23		Strict-Transport-Security max-age=31536000; includeSubDomains; preload
24		X-Content-Type-Options nosniff
25		X-Frame-Options DENY
26	}
27	route {
28		header Cache-Control max-age=3600
29		header /img/* Cache-Control max-age=31536000
30		header /ttf/* Cache-Control max-age=31536000
31		header /manifest.json Cache-Control max-age=31536000
32		header /favicon-32x32.png Cache-Control max-age=31536000
33	}
34
35	handle_errors {
36		@404 {
37			expression {http.error.status_code} == 404
38		}
39		rewrite @404 /404.html
40		file_server
41	}
42	reverse_proxy /img 127.0.0.1:3000
43
44	push / {
45		/ttf/S6uyw4BMUTPHvxk.ttf
46		/ttf/S6u9w4BMUTPHh6UVew8.ttf
47		/img/avatar_huc269450f2cd1dfa314259daca1727106_111505_300x300_resize_box_2.png
48		/manifest.json
49		/favicon-32x32.png
50	}
51
52	push /about/ {
53		/ttf/S6uyw4BMUTPHvxk.ttf
54		/ttf/S6u9w4BMUTPHh6UVew8.ttf
55		/manifest.json
56		/img/avatar_huc269450f2cd1dfa314259daca1727106_111505_300x300_resize_box_2.png
57		/favicon-32x32.png
58	}
59	push /p/*/ {
60		/ttf/S6uyw4BMUTPHvxk.ttf
61		/ttf/S6u9w4BMUTPHh6UVew8.ttf
62		/manifest.json
63		/img/avatar_huc269450f2cd1dfa314259daca1727106_111505_300x300_resize_box_2.png
64		/favicon-32x32.png
65	}
66	log {
67        	output file /var/log/caddy/access_http.log {
68            		roll_size 100mb
69            		roll_keep 10
70            		roll_keep_for 720h
71        	}
72        	format single_field common_log
73        	level INFO
74    	}
75}
1# 在访问路径 / 的时候,将会主动推送花括号中的内容到浏览器中
2
3push / {
4	/ttf/S6uyw4BMUTPHvxk.ttf
5	/ttf/S6u9w4BMUTPHh6UVew8.ttf
6	/img/avatar_huc269450f2cd1dfa314259daca1727106_111505_300x300_resize_box_2.png
7	/manifest.json
8	/favicon-32x32.png
9}

如果你对于HTTP/2在网页中的实际应用不太了解的话,可以看看这一篇Cloudflare的博文–Better HTTP/2 Prioritization for a Faster Web。

静态资源优化

静态资源压缩

现在HTML、CSS、JS、图片压缩应该是个前端都会做了,这里也不详细说了。讲一下我自己博客在用的工具。

HTML: 使用了minify

CSS: 使用了 clean-css,压缩的效果是要比PostCSS要好。在clean-css的level2的级别下,效果最好。

JS: 原JS => Babel => ESBuild

图片:先用了Google官方提供的cwebp转成webp,再使用TinyPNG压缩一次。

什么?浏览器不支持webp?我没听到。

减少静态资源体积

现在的项目手脚架那么多,肯定都已经让压缩资源成为常态化。不管你会不会,框架都会帮你搞定这个事情,就是看压缩的程度如何罢了。

但是,我们可以从开发的角度,通过按需引入直接去减少引入资源的体积,从根本上解决问题。

譬如:

  • 现在很火的Tailwind CSS,就支持在Production下将不用的CSS移除。主要是通过正则表达式的方式来确定是否有使用该类名。

  • 对于JS的处理,可以多使用 Tree Shaking,将没有用到的内容去除。

  • 引入CSS框架bulma的时候,应该按需引入-重新打包,而不是直接引入CDN的链接到网页中。

关键渲染路径(Critical Rendering Path)

接下来的内容,还需要您了解什么是关键渲染路径(Critical Rendering Path)。

如果你对关键渲染路径没有一点了解的话,那么我十分建议您先看Google出品的视频教程 —— Web Performance Optimization 。

如果您不想看,那么就大致参考下面的内容来阅读文章:

常规的网页解析流程,其实非常粗糙,仅供参考:

  1. 解析HTML,有css 或者 js资源,就发起http请求去获取
  2. 解析CSS,生成 CSS Object Model。此时同步的 JS被block。
  3. 执行同步的JS代码
  4. 将 DOM Tree 和 CSS Object Model 合并生成Render Tree
  5. Layout阶段,分析以及计算元素的布局
  6. Paint,绘制每一个pixel

优化静态资源加载顺序 – 我比浏览器聪明(bushi)

上面提到的所有优化手段都是我们可以直接控制的部分,下面还有一个更加不可控的事情,那就是浏览器的资源加载。

虽然都是W3C的标准,但是不同浏览器是不同厂商写出来的,对于资源加载的优先级或者带宽分配都是不一样的。

这一部分可以说是最不可控的部分。

GIF动图实际上反应了,不同浏览器对于资源的加载有着不同优先级以及不同的带宽分配。也可以侧面地看到Edge究竟有多拉。

用户体验: Chrome => Firefox => Safari => Edge

Source: Better HTTP/2 Prioritization for a Faster Web

All of the current browser engines implement different prioritization strategies, none of which are optimal.

Source: Better HTTP/2 Prioritization for a Faster Web

博文中吐槽,没有一个好的。都有博主不想要的效果。2333

在这里简单描述下它们的缺点:

  • Edge && IE: Edge和IE不支持资源优先级排序,会并行加载所有资源。然后就是一直白屏,直到全部加载完成,一起显示。现在的Edge转用Chromium真是明智。
  • Safari:跟Edge类似,但是会通过将带宽分配给CSS资源,然后使得内容更快得显示出来。
  • Firefox:会构建一个依赖树,会对资源进行分组。然后将组进行排序,一个组会接着一个组加载或者多个组并行。在组内资源会共享带宽地下载资源。阻塞脚本(Block Script)和CSS并行加载,并没有起到一个管道化的效果。即CSS文件没有被优先加载。
  • Chrome:几乎和最佳的效果一样。资源加载的优先级完全符合网页的解析流程,但是图片的加载并不是并行的,是一张张加载的。

常规的网页解析流程,其实非常粗糙,仅供参考:

  1. 解析HTML,有css 或者 js资源,就发起http请求去获取
  2. 解析CSS,生成 CSS Object Model。此时同步的 JS被block。
  3. 执行同步的JS代码
  4. 将 DOM Tree 和 CSS Object Model 合并生成Render Tree
  5. Layout阶段,分析以及计算元素的布局
  6. Paint,绘制每一个pixel

从上面的信息就可以知道,除了Chrome之外,其他的浏览器真的很蠢。没有对比就没有伤害。

不同的加载顺序,对于用户的体验来说真的差太多了。所以,我们应该要手动介入资源的加载顺序,免得浏览器瞎搞。

我们现在目标就是GIF图中的Optimal所展示的样子。

  1. 优先加载网页的骨架 —— HTML 、CSS
  2. 执行同步JS脚本 —— Blocking Script
  3. 加载字体 —— Fonts
  4. 加载首屏图片 —— Image
  5. 执行异步脚本—— Async Script

APP Shell 和 Skeleton

在看接下来的内容的时候,我希望您先对APP Shell以及Skeleton有一点的了解。

可以直接观看下面的哔哩哔哩视频,理解一步到位。

相关资料:


从上面的资料,我们就可以知道无论是APP Shell 还是 Skeleton的概念的提出都是为了用户的体验而生的。

让用户在弱网的时候能够先看到一部分东西,缓解焦虑。

那么我们现在要优化博客,要优先加载网页骨架,那么不就是等于在做一个APP Shell吗?

内容可以先不出来,但是框架一定要先出。

App Shell是支持用户界面所需的最小的 HTML、CSS 和 JavaScript,如果离线缓存,可确保在用户重复访问时提供即时、可靠的良好性能。

实际上,静态博客和动态站点做App Shell是有一些不一样的。但是核心都是一样的,都是为了关键渲染路径最短,关键内容可以快速显示。

静态博客很多时候中间的文章内容较多,进而会导致页面的**First Contentful Paint (FCP)**加长。优化方法,我们后续再提。

是的,这是一个PWA的内容。后续我会讲到Service Worker。

那么我们只需要对关键渲染路径(Critical Rendering Path)下手,保证关键资源(HTML、CSS、Javascript)被第一时间执行,不被任何其他的事件阻塞即可。需要第一时间用到的,才叫做关键资源。

head标签

谢天谢地,感谢您能看到这里。这里开始真正的实操了。

记住我们的目标: 保证关键资源(HTML、CSS、Javascript)被第一时间执行,不被任何其他的事件阻塞即可。需要第一时间用到的,才叫做关键资源。

资源引入

  • 不要放任何的JS脚本在head,除非你的JS需要直接影响到页面的渲染,譬如操作一些CSS的渲染变量。
  • 不要直接使用script标签引入外部JS脚本,哪怕你使用了defer async。因为历史的原因,这两个属性根本不能保证脚本是异步加载。引入操作,请看后续部分的内容。
  • 如果页面中的样式比较少,应该内联页面需要的CSS样式在style标签中,避免再通过网络请求一次CSS。减少一次RTT,在网页加载的初期尤为重要。
  • 如果想要非阻塞地引入CSS资源,可以用下面的方式。
1<link rel="stylesheet" href="defer.css" media="print" onload="this.media='all';this.onload=null">
2<noscript><link rel="stylesheet" href="defer.css"></noscript>

网络优化

  • 因为通过 HTTPS 加载的页面上内嵌链接的域名并不会执行预加载。所以需要设置一下meta。
1<meta content="on" http-equiv="x-dns-prefetch-control">
  • 强制查询特定主机名,当频繁使用某些域名的时候,可以预先解析DNS。也可以将需要用到的域名全都加进去。因为DNS的解析是比较耗时的,200ms+。
1<link rel="dns-prefetch" href="http://www.domain.com/">
 1https://www.mi.com/index.html
 2
 3<meta http-equiv="x-dns-prefetch-control" content="on">
 4<link rel="dns-prefetch" href="//s01.mifile.cn" />
 5<link rel="dns-prefetch" href="//c1.mifile.cn" />
 6<link rel="dns-prefetch" href="//i3.mifile.cn" />
 7<link rel="dns-prefetch" href="//i2.mifile.cn" />
 8<link rel="dns-prefetch" href="//i1.mifile.cn" />
 9<link rel="dns-prefetch" href="//i8.mifile.cn" />
10<link rel="dns-prefetch" href="//v.mifile.cn" />
11<link rel="dns-prefetch" href="//a.huodong.mi.cn" />
  • 预先建立连接。当浏览器需要发送请求给一个网站的时候,是需要做DNS解析、协商SSL等工作,即Initial connection。那么我们可以让浏览器提前与CDN建立连接,方便我们后续使用。
1<link crossorigin="anonymous" href="https://cdn.jsdelivr.net" rel="preconnect">
  • 利用好Link Prefetch,使用该属性,可以提前加载需要的资源。后面会有第三方包来利用这个属性。
1<link rel="prefetch" href="https://blog.zsnmwy.net/tags/%E7%BC%BA%E6%B0%A7/">
  • 利用preload属性,对准备使用的脚本进行低优先级的预加载。
1<link rel="preload" href="https://cdn.jsdelivr.net/npm/instant.page@5.1.0" as="script">

body底部前的script标签

  • 加载非必须的第三方JS,应该在load事件的时候,调用下面的loadScript函数来确保异步加载脚本。
 1// 谢谢苏卡卡大佬的代码
 2// https://blog.skk.moe/
 3function loadScript(url, cb, isMoudule) {
 4  var script = document.createElement('script');
 5  script.src = url;
 6  if (cb) script.onload = cb;
 7  if (isMoudule) script.type = 'module';
 8  script.async = true;
 9  document.body.appendChild(script);
10}
  • 避免耗时函数直接在script中操作。同步JS是会阻塞整个渲染路径,导致白屏的出现。应当将耗时函数包在setTimeout(() => {}, 0)之中。然后就可以将函数放在Paint后面执行。
1// 依旧感谢苏卡卡大佬教会我这个Hack
2
3setTimeout(() => {
4  console.log('long time fun ...')
5  const start = new Date();
6  while(new Date() - start < 10000) {}
7}, 0);
  • 使用window.requestAnimationFrame改善DOM操作。可以减低响应时间,影响较大。可以将DOM操作都封装成一个函数,再使用该函数回调。
 1// 感谢苏卡卡大佬教会我这个操作
 2
 3const element = document.getElementById('some-element-you-want-to-animate');
 4let start;
 5
 6function step(timestamp) {
 7  if (start === undefined)
 8    start = timestamp;
 9  const elapsed = timestamp - start;
10
11  //这里使用`Math.min()`确保元素刚好停在200px的位置。
12  element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
13
14  if (elapsed < 2000) { // 在两秒后停止动画
15    window.requestAnimationFrame(step);
16  }
17}
18
19window.requestAnimationFrame(step);

缩短**First Contentful Paint (FCP)**时间 / 减少白屏时间

优化思路来自于苏卡卡大佬

  1. App Shell 与主体内容分离。
  2. 优先加载App Shell 内容(HTML CSS),触发FCP,显示内容。
  3. APP Shell 中做好Loading动画。
  4. 异步加载主体的HTML以及CSS。
  5. 最后,显示主体内容。

通过上面的优化,我相信你已经可以将网页优化得很快了。

但是对于静态或者动态生成页面的博客来说,这还不够。

因为这类的博客的HTML都是完整的,包括了APP Shell以及文章内容。

对于大部分的博客来说,中间的文章内容必定是占大头。

如果将其纳入到App Shell的范围内,那么就意味着Render Tree大了很多。

Render Tree 大了就直接导致了后续的Layout 和 Paint 工作量大,进而直接拉长了FCP时间。


那么有什么办法来解决这个问题?

很简单,就使用上面视频–Web Performance Optimization提到的display: none,对页面的Render Tree进行分割。

因为在构建Render Tree的时候,碰到了display: none属性的时候,就会忽略掉这个属性下面的所有 Dom 节点。进而实现了对于Render Tree的裁剪。


将CSS分成两部分:App Shell 以及 文章主体。

  1. 在 App Shell 的时候将文章主体直接声明为 display: none
  2. 通过异步加载文章主体CSS。
  3. 在文章主体CSS中,再将文章主体声明为display: block

我是建议直接将APP Shell的CSS直接内联在HTML中。

这样子就可以保证在加载完HTML的时候,就可以立即显示出APP Shell。

可以恰当地在App Shell做一些loading动画,可以让弱网用户不那么着急。


为了New Feature,肯定会带来New BUG。

主要的问题还是因为display属性带来了布局的变化。

  • 滚动条消失 – 在html加入overflow-y: scroll;
  • 已经渲染过的APP Shell 重新渲染 – 通过加入contain属性解决问题。具体看MDN文档使用。

CSS contain 属性允许开发者声明当前元素和它的内容尽可能的独立于 DOM 树的其他部分。这使得浏览器在重新计算布局、样式、绘图、大小或这四项的组合时,只影响到有限的 DOM 区域,而不是整个页面,可以有效改善性能。

改善长文章的渲染性能

上面一节只是避免了文章区域对于App Shell的影响,但是并没有实际上解决文章区域的渲染耗时的问题。

不做任何处理的情况下,长文章的全部内容是会参与布局计算以及绘制,导致Rendering 占时过久。

但实际上,后面的内容我们都还没有看,就已经渲染完了。

为了避免这种问题,推出了一个新属性–content-visibility。

The content-visibility property controls whether or not an element renders its contents at all, along with forcing a strong set of containments, allowing user agents to potentially omit large swathes of layout and rendering work until it becomes needed.

Source: https://www.w3.org/TR/css-contain-2/#content-visibility

当一个容器被该属性声明之后,如果容器不在viewpoint中,则容器中的子元素不会参与到Render Tree 和Layout的计算当中。等于避免了没有呈现内容也进行了渲染操作。

实际的使用就是将文章的内容分成一个个块,并且在块上面加上属性content-visibility。

博客的底部footer,我认为也是可以加上的。

如果只是指定了content-visibility的话,那么会使得容器的高度只有0。因为内容的出现,会使得进度条重新计算长度,导致进度条疯狂地跳跃。

我们应该通过contain-intrinsic-size指定宽度和高度,给容器假定一个宽高。浏览器未真正计算该布局的时候,都会直接使用这个宽高。contain-intrinsic-size定义的值,无需实际考虑容器的实际大小。

1//具体的用法会出现在后续的DEMO中。
2.story {
3  content-visibility: auto;
4  contain-intrinsic-size: 1000px;
5}

相关资料:

避免图片抖动而引发性能问题

lazy-img-without-placeholder.gif

通过观察上面的GIF就可以看出一个问题:图片的后续加载,导致了整体的页面布局发生了改变。

发生抖动的时候,浏览器会重新计算布局等一系列操作,导致性能的浪费。

很多文章都提到了使用一个Aspect Ratio Boxes来解决问题。

通过创建一个响应式的容器,使得整体的页面布局稳性下来。

如果你定死了宽高,那对于响应式布局来说是很不友好的。

1// source: https://css-tricks.com/aspect-ratio-boxes/
2<h1 class="aspect-ratio-box">
3  <div class="aspect-ratio-box-inside">
4    Happy Birthday
5  </div>
6</h1>
 1// source: https://css-tricks.com/aspect-ratio-boxes/
 2.aspect-ratio-box {
 3  height: 0;
 4  overflow: hidden;
 5  padding-top: 591.44px / 1127.34px * 100%;
 6  background: white;
 7  position: relative;
 8}
 9.aspect-ratio-box-inside {
10  position: absolute;
11  top: 0;
12  left: 0;
13  width: 100%;
14  height: 100%;
15}

想看具体的代码方案,请移步到下面的两篇文章。有些轮子是真的不想造了啊。。。

解决之后:

lazy-img-with-placeholder.gif

页面的布局不再会因为图片的加载而引发抖动。

网络层面

一定要配置HTTP/2 Server Push,加快静态资源的推送。

优化第三方重磅JS

在这里非常感谢苏卡卡大佬的开源。没有大佬就没有我这篇流水文章。

  • Google Analytics

谷歌的网站分析,无论是引入的JS大小、被各种拦截广告软件block、缓存时间少或者是网络经常抽风等原因,确实是应该找个替代方案了。

我们可以使用苏卡卡大佬的一个开源项目,cloudflare-workers-async-google-analytics。通过CF Worker中转数据到Google。

  • DISQUS

这是一个很出名的评论系统,但是在国内确实是拉。具体可以看苏苏卡大佬的解决方案,我在这里就不搬运了。

使用Service Worker来缓存页面资源

Service workers 非常强大,因为他们可以控制网络请求、修改网络请求、返回缓存的自定义响应,或者合成响应。

Source: https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps/Offline_Service_workers

Service Worker 是PWA里面的一个关键技术,他并不会阻塞主进程。

这里主要是用它来做缓存功能,通过service worker去手动介入资源的缓存。不仅仅局限于web服务器的Cache-Control字段。

现在已经有现成的工具箱–workbox,没必要从0开始写。

我们只需要写好正则表达式做好匹配策略以及在workbox中选择好缓存策略,然后再恰当地使用一下官方的Plugin就好了。

可以通过访问网页路径 /sw.js ,获取本站的Service Worker配置。可以对照下面的内容进行阅读。本站的配置并不是最佳实践!!!

缓存策略

详细的缓存策略可以阅读官方文档 —— Workbox Strategies。下面简单提一提预设的策略。

  • Stale-While-Revalidate

如果有Cache的情况下,会优先使用Cache返回。并且会在响应Page之后发起网络请求更新Cache中的内容。

如果没有Cache,则直接返回网络请求。

如果你将其规则应用到网页HTML上面的话,那么会导致你的需要重复刷新两次才可以显示最新内容。

因为博客少更新,所以本站就应用了该规则在页面的HTML上。

  • Cache First (Cache Falling Back to Network)

如果有Cache,则会立即返回Cache。并且后续不会使用到网络来更新Cache的内容。

没有Cache,则直接返回网络请求。并且返回的内容会进行Cache。

可以使用该规则来缓存一些不容易改变的东西,如图片、视频等。

  • Network First (Network Falling Back to Cache)

默认情况下,它将尝试从网络获取最新的响应。

如果请求成功,它将把响应放到Cache中。

如果网络未能返回响应,则将使用Cache的内容。

新闻列表用这个策略,感觉也挺好的。

  • Network Only

只使用网络响应页面的请求。

例如Google Analytics就可以使用这个策略。

  • Cache Only

只使用Cache响应页面的请求。

感觉很少用。

  • Custom Strategies

自定义策略就麻烦自行看看官方的文档啦。

Plugins

官方文档指路 — Workbox Plugins

下面就不翻译了 偷懒

  • BackgroundSyncPlugin

If a network request ever fails, add it to a background sync queue and retry the request when the next sync event is triggered.

1// Example
2new BackgroundSyncPlugin('myQueueName', {
3    maxRetentionTime: 24 * 60 // Retry for max of 24 Hours (specified in minutes)
4});
  • BroadcastUpdatePlugin

Whenever a cache is updated, dispatch a message on a Broadcast Channel or via postMessage().

 1// Example
 2new BroadcastUpdatePlugin({
 3  headersToCheck: ['etag'],
 4  generatePayload(data) {
 5    return {
 6      cacheName: data.cacheName,
 7      updatedURL: data.request.url,
 8    };
 9  },
10})
  • CacheableResponsePlugin

Only cache requests that meet a certain criteria.

只缓存指定的响应状态。

1// Example
2new CacheableResponsePlugin({
3        statuses: [0, 200],
4}),
  • ExpirationPlugin

Manage the number and maximum age of items in the cache.

用于指定过期时间。

 1// Example
 2new ExpirationPlugin({
 3  // 最大存储数量
 4  maxEntries: 50,
 5  // 最大过期时间
 6  maxAgeSeconds: 30 * 24 * 60 * 60,
 7  // 超过存储容量时自动清理
 8  purgeOnQuotaError: true,
 9})
10
11// Example
12new ExpirationPlugin({
13  maxEntries: 4,
14  maxAgeSeconds: 365 * 24 * 60 * 60, // 365 days
15  purgeOnQuotaError: !0,
16})
  • RangeRequestsPlugin

Respond to requests that include a Range: header with partial content from a cache.

Range这个请求头,一般出现在视频文件的在线播放或者音频文件的在线播放。可以通过这个请求头获取到文件的特定位置。

 1routing.registerRoute(
 2  ({ url }) => url.pathname.endsWith('.mp4'),
 3  new CacheFirst({
 4    cacheName: 'media-cache',
 5    plugins: [
 6      new CacheableResponsePlugin({ statuses: [200] }),
 7      new RangeRequestsPlugin()
 8    ]
 9  })
10)

使用

  1. 引入workbox

引入的方法很多,譬如通过CDN、Webpack直接打包等。反正我是非常不推荐通过Google的官方CDN来导入,经常抽风。

1// 通过jsdelivr CDN 引入。实际上 5.1.3的版本已经落后官方一个大版本了。但是他没有更加新的版本了。
2importScripts(
3  "https://cdn.jsdelivr.net/npm/workbox-cdn@5.1.3/workbox/workbox-sw.js"
4  );
5
6workbox.setConfig({
7  modulePathPrefix: "https://cdn.jsdelivr.net/npm/workbox-cdn@5.1.3/workbox/",
8});
9
1# 本地安装 workbox-cli并提取库到本地路径
2
3$ npm i workbox-cli -g
4# Or in Yarn
5$ yarn global add workbox-cli
6# After installation, run copyLibraries to get a copy of workbox libs
7$ workbox copyLibraries {path/to/workbox/}
1// 本地URL导入
2
3importScripts(
4  "/workbox-v6.1.5/workbox-sw.js"
5);
6
7workbox.setConfig({
8  modulePathPrefix: "/workbox-v6.1.5/",
9});

  1. navigator.serviceWorker.register
1const denyExec = (fun) => setTimeout(fun, 0);
2
3function registerServiceWorker() {
4      "serviceWorker" in navigator && navigator.serviceWorker.register("/sw.js");
5}
6
7"complete" === document.readyState
8      ? denyExec(registerServiceWorker)
9      : window.addEventListener("load", () => denyExec(registerServiceWorker));

请确保你的SW不要阻塞页面的渲染。


  1. precache
1precaching.precacheAndRoute([
2  { url: "https://cdn.jsdelivr.net/npm/instant.page@5.1.0", revision: null},
3  { url: "https://cdn.jsdelivr.net/npm/cfga@1.0.3", revision: null},
4  { url: "/ttf/S6uyw4BMUTPHvxk.ttf", revision: null },
5  { url: "/ttf/S6u9w4BMUTPHh6UVew8.ttf", revision: null },
6  { url: "/manifest.json", revision: null },
7  { url: "/favicon-32x32.png", revision: null },
8]);

此处的资源是一般是放置App Shell 需要用到的资源。

precache是在sw的Install环节进行的,会直接对资源链接进行缓存。

如果出现任何的错误,则会直接导致sw安装失败。

在配置的过程中,要特意留意资源的URL是否可以正确获取。


  1. routing.setDefaultHandler
1/*
2 * Default - Serve as it is
3 * staleWhileRevalidate
4 */
5routing.setDefaultHandler(
6  new StaleWhileRevalidate()
7);

设置默认资源应该是使用什么策略来缓存。我这里倾向于使用StaleWhileRevalidate。

  1. routing.registerRoute
 1// 版本号
 2const cacheSuffixVersion = "-2104300145"
 3// 注册路由 跟平常的路由差不多,匹配规则 + 处理规则
 4routing.registerRoute(
 5  // 资源匹配规则
 6  new RegExp(/.*ttf/),
 7  // 策略, 缓存第一,优先返回缓存
 8  new CacheFirst({
 9    // 存放资源的database名字
10    cacheName: "static-immutable" + cacheSuffixVersion,
11    plugins: [
12      // 过期插件
13      new ExpirationPlugin({
14        // 最大存储数量
15        maxEntries: 50,
16        // 最大过期时间
17        maxAgeSeconds: 30 * 24 * 60 * 60,
18        // 超过存储容量时自动清理
19        purgeOnQuotaError: true,
20      }),
21    ],
22  })
23);

这里只是一个路由定义、指定策略以及使用插件的例子。

网页内容预加载

我们可以使用一个npm包instant.page,实现网页内容预加载。

instant.page 主页

他的原理则是,当鼠标停留/手机屏幕显示超过一定时间的时候,在body处添加一个prefetch标签内容的预加载。

1<link rel="prefetch" href="https://zsnmwy.net/p/%E5%BF%AB-%E5%BF%AB-%E5%BF%AB-%E5%A6%82%E4%BD%95%E6%8B%A5%E6%9C%89%E4%B8%80%E4%B8%AA%E7%A7%92%E5%BC%80%E7%9A%84%E5%8D%9A%E5%AE%A2/">

用户体验提升非常好。

后记

其实这一篇文章非常之长,花费了我很多天的时间。

但是我希望这一篇文章可以有效地帮助从没有做过网页优化过的同学摸到一点网页优化的门槛。

可能文章的内容会出很多问题,但这也算是一个小总结。或者说是一个错误的小总结?

愿你,天天开心,天天快乐~~