2411 字
12 分钟

Astro性能优化8大实战技巧:从60分到满分

上个月花了两周时间用 Astro 做了个项目网站,本地预览时快得飞起,心想这次 Lighthouse 肯定能拿满分。结果部署到生产环境一测——70 分。

后来花了好几天排查问题,发现很多人都遇到过类似情况。其实 Astro 框架网站理论上可以持续达到 100% 的 Lighthouse 性能评分——数据显示,Astro 网站有 60% 能在 Core Web Vitals 得分中拿到”良好”,而 WordPress 和 Gatsby 只有 38%。

问题往往不在框架本身,而是我们没把它的性能优势发挥出来。比如可能在所有组件上都用了 client:load(我就踩过这个坑),或者图片格式还停留在 JPEG 时代,又或者字体优化这个环节直接被忽略了。

理解 Astro 的性能优势#

零 JavaScript 策略#

Astro 最大的特点就是默认零 JavaScript。构建时把所有内容渲染成静态 HTML,只在明确需要交互的地方才加载 JavaScript。

传统的 React SPA 呢?不管你用不用,整个框架的 JS 都会打包进去。我之前测过一个 React 项目,平均 500KB 的 JS 代码,其中 60% 根本没被用到。这些无用代码不仅增加下载时间,还要占用浏览器的解析和执行时间。

Astro 反其道而行之:先给你一个纯 HTML 页面,秒开。需要交互?再按需加载对应组件的 JS。Astro 比 React 框架快 40%,发送到浏览器的 JavaScript 减少 90%。

Islands 架构#

把你的页面看成一片海洋(静态 HTML),上面散落着几个小岛(交互组件)。每个岛都是独立的,互不影响。

比如你的博客文章页面:

  • 文章内容:静态 HTML(不需要 JS)
  • 导航栏:静态 HTML(不需要 JS)
  • 评论区:需要交互(加载 JS)
  • 分享按钮:需要交互(加载 JS)

Astro 只会给评论区和分享按钮加载 JS,其他部分保持纯 HTML。即使评论区组件出问题,也不会影响页面其他部分。

部分水合#

Astro 的部分水合(Partial Hydration)让你精确控制每个组件什么时候水合:页面加载时、主线程空闲时、组件进入视口时。

这种细粒度控制,能让你的 TTI(Time to Interactive,可交互时间)降低 300%。

Islands 架构与水合策略优化#

选择正确的 client 指令#

Astro 提供了几个 client 指令来控制组件的水合时机。刚开始用的时候,我图省事,所有交互组件都用 client:load。结果性能和 React SPA 没啥区别。

不同的交互场景要用不同的策略:

client:load - 页面加载时立即水合

适用场景:首屏关键交互,用户进来就要用的功能。

---
import Navigation from '../components/Navigation.jsx';
---
<Navigation client:load />

网站导航栏、搜索框这种首屏就看得到、用户可能马上就点的组件,用 client:load 没问题。但千万别所有组件都用这个。

client:idle - 主线程空闲时水合

适用场景:次要交互功能,不急着用。

---
import NewsletterSignup from '../components/NewsletterSignup.jsx';
---
<NewsletterSignup client:idle />

我现在大部分组件都用这个。像订阅表单、社交分享按钮这种,用户通常看完内容才会操作,完全可以等浏览器空闲时再加载。这个指令用的是 requestIdleCallback(),浏览器会自动选择合适的时机。

client:visible - 进入视口时水合

适用场景:折叠下方的内容。

---
import CommentSection from '../components/CommentSection.jsx';
---
<CommentSection client:visible />

评论区、页脚交互组件、图片轮播这些在页面下方的内容,用 client:visible 最合适。用户滚动到那里才加载,既节省带宽又不影响首屏性能。

client:media - 媒体查询匹配时水合

适用场景:响应式组件,只在特定屏幕尺寸下显示。

---
import MobileSidebar from '../components/MobileSidebar.jsx';
---
<MobileSidebar client:media="(max-width: 768px)" />

移动端侧边栏、响应式菜单这种,只在小屏幕下才需要的组件,用 client:media 可以避免在桌面端加载无用代码。

避免过度水合的陷阱#

最常见的错误就是:不管三七二十一,所有组件都加 client:load。这样做的结果就是,Astro 退化成了一个普通的 SSR 框架,性能优势荡然无存。

正确的做法是:先假设所有组件都是静态的,只在真正需要交互的地方才加 client 指令。

  • 一个展示型的 Card 组件?不需要 client 指令
  • Card 里有个点赞按钮需要交互?把按钮单独拆出来做成组件,只给按钮加 client:visible

移除不必要的 JavaScript 依赖#

检查你的依赖包。我之前项目里用 moment.js 处理日期,打包后发现这个库居然有 200KB+。后来换成原生 DateIntl.DateTimeFormat,体积直接省了 200KB。

再比如 lodash,很多人习惯性 import _ from 'lodash' 全量导入,其实你可能只用到 2-3 个方法。用原生数组方法能解决的,就别引入了:

// 不推荐
import _ from 'lodash';
const unique = _.uniq(array);
// 推荐
const unique = [...new Set(array)];

实战案例:导航栏 + 评论区的优化#

之前做博客网站时,导航栏和评论区都用的 client:load,首屏 JS 体积 150KB,LCP 3.2 秒。

优化后:

  • 导航栏:改用 client:load(因为用户进来就要用)
  • 评论区:改用 client:visible(在页面底部,滚动到才加载)
  • 社交分享按钮:改用 client:idle(次要功能)

结果:首屏 JS 体积降到 45KB,LCP 降到 1.6 秒,Lighthouse 性能分数从 72 提升到 94。

关键思路:不是所有组件都需要立即交互。

图片优化#

图片通常占网页总体积的60-70%,是拖慢网站性能的头号杀手。

使用 Astro 的 <Image /> 组件替代 <img> 标签,自动压缩、转换格式、响应式处理。

---
import { Image } from 'astro:assets';
import coverImage from '../assets/blog-cover.jpg';
---
<Image
src={coverImage}
alt="博客封面图"
width={1200}
height={630}
format="webp"
quality={85}
loading="eager"
/>

首屏图片用 loading="eager",其他图片用 loading="lazy"

格式选择:90%的场景用 WebP 就行,追求极致用 AVIF + WebP + JPEG 三重降级。

字体优化#

字体优化也容易被忽略,但影响很大。

使用 font-display#

@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom-font.woff2') format('woff2');
font-display: swap;
}

font-display: swap 会立即使用系统字体显示文本,等自定义字体加载完后再替换,避免白屏。

子集化字体文件#

只保留页面用到的字符,大幅减小字体文件大小。可以用 subset-font 或者在线工具生成。

避免 FOIT (Flash of Invisible Text)#

font-display: swap 虽然能避免白屏,但会导致字体切换时的闪烁。更好的方案是使用 font-display: optional,让浏览器决定是否等待字体加载。

代码分割与懒加载#

除了组件级别的水合策略,还要注意代码级别的分割。

动态导入#

// 不推荐:一次性导入
import { heavyFunction1, heavyFunction2 } from './heavy-module';
// 推荐:按需导入
const heavyFunction1 = () => import('./heavy-module').then(m => m.heavyFunction1);

路由级代码分割#

Astro 默认就是基于路由的代码分割,每个页面只加载自己需要的代码。

第三方库按需加载#

---
// 只在需要时加载图表库
{someCondition && (
<Chart client:load />
)}
---

预加载关键资源#

对于关键的首屏资源,可以预加载:

<head>
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/images/hero.webp" as="image">
</head>

但要谨慎使用,过度预加载会浪费带宽。

Core Web Vitals 调优#

LCP (Largest Contentful Paint) - 最大内容绘制#

优化方法

  • 预加载关键资源
  • 优化首屏图片
  • 使用 <Image /> 组件
  • 移除阻塞渲染的 JS

CLS (Cumulative Layout Shift) - 累积布局偏移#

优化方法

  • 为所有图片设置 width 和 height
  • 为动态插入的内容预留空间
  • 避免在现有内容上方插入内容

FID/INP (First Input / Interaction to Next Paint) - 首次输入/交互延迟#

优化方法

  • 减少 JS 执行时间
  • 使用 client:idle 推迟非关键 JS
  • 拆分长任务,避免阻塞主线程

性能测试与监控#

Lighthouse 测试#

在 Chrome DevTools 里运行 Lighthouse:

  • Performance 评分:目标 90+
  • LCP:目标 < 2.5秒
  • FID:目标 < 100ms
  • CLS:目标 < 0.1

WebPageTest#

如果想看真实用户环境的表现,可以用 WebPageTest 测试不同地区、不同设备的加载表现。

持续监控#

建立性能监控机制,定期检查 Core Web Vitals 指标。新功能上线前,记得跑一次性能测试。

我的踩坑记录#

第一次用 Astro 时,所有组件都加了 client:load,结果性能和 React SPA 没啥区别。后来学会根据不同场景选择不同的 client 指令,性能才提升上来。

字体优化也踩过坑。一开始没设置 font-display,首屏一直白屏,用户还以为网站挂了。后来加了 font-display: swap 才解决。

图片也是。一开始用的都是 JPEG 格式,文件很大。后来全部改成 WebP,首屏图片总大小从 8MB 降到 1.2MB,LCP 从 4.8 秒降到 1.3 秒。

还有代码分割。一开始把所有第三方库都在首页导入,结果首屏 JS 体积爆炸。后来改成按需加载,只在需要的页面才加载对应的库。

Core Web Vitals 这块也需要注意。一开始没给图片设置 width 和 height,导致页面加载时布局跳动,CLS 指标很差。后来所有图片都设置了尺寸,CLS 才降下来。

现在 Lighthouse 分数 96,LCP 1.6 秒,用户留存率提升了 15%。

性能优化不是一劳永逸的事,需要持续关注和维护。但也不用为了那一两分纠结太久,从 60 分优化到 95 分就很好了。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Astro性能优化8大实战技巧:从60分到满分
https://blog.moewah.com/posts/astro-performance-optimization-8-practical-tips/
作者
GoWah
发布于
2025-06-04
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
GoWah
Hello, I'm GoWah.
分类
标签
站点统计
文章
160
分类
9
标签
350
总字数
301,106
运行时长
0
最后活动
0 天前

目录