Astro 实战:八步优化突破移动端 PageSpeed 100
通过岛屿架构、图片压缩、CSS 清理等组合优化,Astro 站点从 80+ 分突破到 100 分;关键是多个小优化的叠加而非单点魔法。
通过岛屿架构、图片压缩、CSS 清理等组合优化,Astro 站点从 80+ 分突破到 100 分;关键是多个小优化的叠加而非单点魔法。
还在努力让 PageSpeed Insights 达到那个似乎遥不可及的 100 分吗?
在一个真实的客户项目中,我卡在 80 多分到 90 出头的区间,耗费的时间长得让我都不太愿意承认。我尝试了网上推荐的所有方法——压缩图片、压缩 JavaScript、懒加载、移除未使用的 CSS——但分数几乎纹丝不动。
移动端才是真正的挑战。Lighthouse 似乎打定主意要在那里给你上一课。
最终,我们在 waxedoc.com 上做到了。这是我使用 Astro 为一家脱毛工作室搭建的网站。
(所有测试都在无痕窗口中完成,以免浏览器扩展影响结果。)
有意思的是,并不存在某个神奇的优化手段。
真正起作用的是一系列细小的改进,它们叠加在一起,最终带来了巨大的变化。
这听起来多少有点胜之不武,但事实确实如此。
Astro 默认渲染静态 HTML,只会通过 Islands Architecture 对你明确设置为可交互的组件进行 hydration。
对于这个网站的大多数页面,浏览器在初始加载期间几乎不需要下载任何 JavaScript。
如果你构建的是一个内容密集型网站,那么在还没优化任何一行代码之前,你就已经抢占了先机。
这个改动带来的效果远超我的预期。
使用标准的 Google Fonts <link>,意味着浏览器必须:
在你的自定义字体真正开始加载之前,就已经产生了多次网络请求。
于是,我下载了 .woff2 文件,改为在本地托管。
@font-face {
font-family: "Playfair Display";
font-weight: 600;
font-display: swap;
src: url("/fonts/playfair-display-600.woff2") format("woff2");
}
font-display: swap 同样重要——它允许浏览器立即使用后备字体显示文本,而不是在下载自定义字体时让文字保持不可见。
我学到的一点是:浏览器不会仅仅因为看到一条 @font-face 规则,就立即下载对应的字体。
它们首先需要发现,确实有某个元素使用了这种字体。
为首屏使用的字体添加 preload 提示,有助于缩短 LCP。
<link
rel="preload"
as="font"
type="font/woff2"
href="/fonts/playfair-display-600.woff2"
crossorigin
/>
不过需要注意:只预加载真正需要的字体。
我曾经在 CSS 中声明了两个实际上没有在任何地方使用的字体文件。预加载它们只会浪费带宽,还会触发 Lighthouse 关于未使用 preload 的警告。
外部样式表会阻塞渲染。
在浏览器下载并解析它们之前,页面无法完成绘制。
Astro 让这件事变得出乎意料地简单。
// astro.config.mjs
export default defineConfig({
build: {
inlineStylesheets: "always"
}
});
仅这一项设置,就能减少首次绘制前的一次额外网络请求。
这是最容易犯的错误之一。
仅仅添加 sizes 属性还不够。
如果没有正确的 srcset,浏览器仍然只有一张图片可选——这通常意味着手机会下载与桌面端相同的超大图片。
Astro 的 <Image> 组件让这件事变得非常直接。
<Image
src={image}
width={image.width}
height={image.height}
widths={[480, 640, 768, 1024, image.width]}
sizes="(min-width: 1024px) 50vw, 100vw"
/>
对于 Largest Contentful Paint 图片(通常是 hero 图片),还要添加:
fetchpriority="high"
这会告诉浏览器优先下载这张图片,而不是等到加载瀑布流进行到一半时才发现它。
这一点很容易被忽略。
构建过程中生成的带哈希资源是不可变的,因此可以放心地缓存整整一年。
在 Firebase Hosting 中,我添加了:
"headers": [
{
"source": "**/_astro/**",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
这基本等于白捡的性能提升。
box-shadow 或 text-shadow 添加动画这一点让我始料未及。
这些动画看起来非常流畅,但 Lighthouse 将它们标记为非合成动画。
为阴影添加动画,会迫使浏览器在每一帧都重新绘制。
更好的做法是保持阴影静止,只为 opacity 或 transform 添加动画。
.neon {
text-shadow: 0 0 6px currentColor;
animation: flicker 3s infinite;
}
.neon::after {
content: attr(data-text);
text-shadow: 0 0 14px currentColor;
animation: flicker-glow 3s infinite;
}
这样浏览器要做的工作更少。
如果你在滚动或拖动过程中反复调用 getBoundingClientRect() 之类的方法,那么你很可能正在迫使浏览器每秒重新计算几十次布局。
应该只读取一次这些值,然后重复使用。
let box = null;
frame.addEventListener("pointerdown", () => {
box = frame.getBoundingClientRect();
});
frame.addEventListener("pointermove", () => {
if (!ticking) {
requestAnimationFrame(() => {
// use the cached box
ticking = false;
});
}
});
这听起来并不令人兴奋,但这些小改动积少成多,最终会产生显著效果。
并不存在某个单一优化,能让 Lighthouse 分数突然提高 20 分。
真正的过程,是逐个修复一个又一个小问题。
字体加载得快了一点。
图片变小了一点。
渲染开始得早了一点。
JavaScript 阻塞主线程的时间少了一点。
当这些改进积累到足够多时,分数终于达到了 100。
更重要的是,这个网站确实感觉更快了——不只是在 Lighthouse 中如此,真实访客也能感受到。
如果你更想在一个真实的生产网站上查看这些优化,而不是看一个删减过的演示:
<a href="https://waxedoc.com" class="ltag-offer__button crayons-btn crayons-btn--primary">Visit Waxed OC</a>
我很想知道:最近哪个 Lighthouse 审计项最让你难以修复?字体?图片?第三方脚本?还是完全不同的问题?
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。