知识库

按页面分配 JS 预算:Astro islands 的取舍

内容页 0 KB JS,交互只在需要处以 island 形式懒加载。主题切换用内联脚本就够,不必为一个按钮引入整个 React。

预算思维

展示层的原则是按页面分配 JS 预算:阅读类页面发 0 KB 组件 JS,炫技交互 (WebGL、图谱)只出现在首页和相册,且懒加载。Astro 天然适合这套思路—— 默认不发任何组件 JS,只有加了 client:* 指令的 island 才会打包进浏览器。

别把轻交互做重

迁移时最容易犯的错,是把 Next 的「万物皆组件」思维带过来。比如主题切换: Next 版本用了 next-themes + React context + useState/useEffect。但在 Astro 里,这就是一个按钮 + 一段内联脚本的事:

<script is:inline>
  const stored = localStorage.getItem("theme");
  const prefersDark = matchMedia("(prefers-color-scheme: dark)").matches;
  if (stored === "dark" || (!stored && prefersDark))
    document.documentElement.classList.add("dark");
</script>

放在 <head> 首帧执行,避免暗色模式闪烁(FOUC)。为一个按钮引入整个 React 运行时,是典型的预算浪费。

什么时候才用 island

判据很简单:这段交互离开 JS 还能用吗? 能,就用纯 CSS/内联脚本降级; 不能(如 3D 场景),才做成 React island 并 client:visible 懒加载。文字内容 永远写在 island 外面,WebGL 挂了页面也照样可读。

🔍 此前不知道自己不知道的

  • ·Astro 默认不向浏览器发送任何组件 JS,`client:*` 指令是唯一的「JS 开关」
  • ·主题切换这种轻交互用 `is:inline` 脚本即可,引入 next-themes/React 是过度设计
  • ·无闪烁暗色模式的关键是把决定主题的脚本放在 `<head>` 首帧执行

❓ 下次值得追问

  • ·R3F 首页场景用 `client:visible` 懒加载,能否保证 LCP 文本不被 WebGL 阻塞?
  • ·islands 之间需要共享状态时,用 nanostores 还是干脆各自独立?

🔗 关联笔记