Islands Architecture
2022-08-04 · 2,659 chars · 14 min read
本文来自 patterns.dev
TL;DR
Islands Architecture 鼓励在 SSR 的页面中使用小的、聚焦于交互的代码块。Islands 的输出是渐进增强的 HTML,并且在增强方式上更加具体。不同于 SPA 的全页面渲染,Islands 有多个入口点。这些可交互 Islands 的 JS 代码可以独立请求并执行 hydrate,同时允许页面的其余部分只是静态 HTML。
加载和执行过多的 JS 是很伤性能的。然而,一定程度上的可交互性和 JS 是必不可少的,甚至是静态网站。我们之前聊过 SSR 的变体,这些变体可以允许你平衡:
- 与客户端渲染(CSR)相当的交互性
- 与服务端渲染(SSR)相媲美的 SEO 优势
SSR 的核心原理是:在服务端渲染 HTML,并且附带必要的 JS 来完成 hydrate (客户端的水合)。hydrate 是指服务端渲染完成后,在客户端重建 UI 组件的 state 的过程。由于 hydrate 是有成本的,因此每个 SSR 的变体都在尝试优化这个过程。目前主要是通过关键组件的部分、渐进式 hydrate,或组件在渲染时的流式 SSR 来实现的。然而,最终页面需要加载的的 JS 代码量和上述方案并无不同。
“Islands Architecture”(原文 | 译文)这个术语是 Katie Sylor-Miller 和 Jason Miller 推广开来的,用来描述一种范式:通过可基于静态 HTML 独立交付的交互式 islands,达到降低 JS 体积的目的。Islands 是基于组件的架构,提倡通过静态或动态的 islands 划分页面视图。页面的静态区域是非交互式的纯 HTML,不需要 hydrate。动态区域包含 HTML 和能够在渲染后在客户端自行 hydrate 的 JS。
译者注
图片不翻译了,简单解释下:
- 左侧:经典的 SSR,所有组件同时 render + hydrate
- 中间:渐进式 hydrate,服务端 render 所有组件,客户端先 hydrate 关键组件,然后逐渐 hydrate 其他组件
- 右侧:Islands Architecture,静态部分服务端渲染,仅加载可交互式组件的 JS(并且每部分都是独立的)
接来下,让我们更详细地探索 Islands Architecture,以及目前可用于实现它的不同选择。
动态组件的 Islands#
大多数页面既有动态内容、也有静态内容。通常,一个页面由静态内容和一些可以隔离开的交互区域组成。例如:
- 博客文章、新闻、主页等。包含带有可交互图片、文字的组件,如内嵌的社交媒体和聊天室
- 电商类的产品页面。包含静态的产品描述、链接、交互式的组件如图片轮播、搜索。
- 典型的银行账户的详情页面。带可交互过滤器的静态列表。
静态内容是无状态的 (stateless),不触发事件,无需在客户端进行 hydrate。服务端渲染后,动态的内容(按钮,过滤器,搜索栏)在客户端侧,必须重新注册相关事件,必须重新生成 DOM(Virtual DOM)。这些 “重新生成”,“hydrate”,“事件注册” 依赖于客户端加载的 JS。
Islands Architecture 更适合在那些,在服务端渲染所有静态内容的页面。在这种场景下,渲染出的 HTML 将包括动态内容的占位符。动态内容占位符包含独立的组件小部件。每个小部件都类似于一个应用程序,包含服务器渲染的输出、和用于在客户端 hydrate 应用程序的 JavaScript。
在 progressive hydration(渐进式水合)模式中,页面的 hydrate 是自上而下的,页面控制各个组件的调度和 hydrate。在 Islands Architecture 中,每个组件都有它的 hydrate 脚本,它是异步执行的,与页面上的任何其他脚本无关。一个组件的性能问题不应该影响其他组件。
译者注
图片不翻译了,简单解释下:
- Page Layout、Blog Title、Blog Content 这三部分,无论是文字还是图片,都是静态的,服务端渲染完,客户端展示即可,不需要 JS 执行
- 下面的两个组件:Comment 和 SocialButton,都是服务端渲染占位符、HTML、JS,然后在客户端执行 hydrate,执行完成后才可以交互。
实现 Islands#
Islands Architecture 借鉴了很多概念,目的在于将他们更好的组织起来。基于模板的静态网站生成工具,如 Jekyll 和 Hugo,支持渲染静态组件。大部分现代前端 JS 框架都支持同构渲染(isomorphic render),允许你在服务端和客户端使用同一套代码。
Jason 的文章建议使用 requestIdleCallback()
来调度组件 hydrate。同构渲染、组件级的部分 hydrate 等功能,都可以构建在框架中,以支持 Islands Architecture。所以,框架应该:
- 支持页面的服务端渲染,无需额外的 JavaScript 代码
- 支持在静态内容中,通过占位符(placeholders)插入独立的动态组件
- 支持组件的同构渲染
你可以使用下面这几个开箱即用的框架
框架#
现在有很多不同的框架支持 Islands Architecture,其中值得关注的是:
Marko#
Marko 是一个开源框架,由 eBay 开发和维护,用于提高服务端渲染的性能。它通过流式渲染(streaming rendering)与自动部分 hydrate 相结合,来支持 Islands architecture。HTML 和其他静态资源,一旦准备好就会立刻流式传输到客户端。交互式组件可以独立、自行完成 hydrate。hydrate 代码仅用于交互式组件,它可以更改浏览器上的 state。它是同构的,Marko 编译器会根据运行位置(客户端、服务器)生成优化后的代码。
Astro#
Astro 是一个静态站点生成工具,可以使用其他框架(如 React、Preact、Svelte、Vue 等)构建的 UI 组件,生成轻量级的静态 HTML 页面。需要客户端 JavaScript 代码的组件会单独加载它们的依赖项。因此,它提供了内置的部分 hydrate。Astro 还可以延迟加载组件。我们下一章节有一个 Astro 的例子。
Eleventy + Preact#
Markus Oberlehner 演示了 Eleventy 的用法,它是一个静态站点生成工具,具有可以部分 hydrate 的同构 Preact 组件。它还支持延迟 hydrate。组件本身以声明方式控制组件的 hydrate。交互式组件使用 WithHydration
修饰器,以便它们在客户端上执行 hydrate。
注意,Marko 和 Eleventy 要早于 Jason 提出 Islands 定义的时间,但包含支持 Islands Architecture 所需的一些功能。Astro 是基于定义构建的,天然支持 Islands 架构。在下一节中,我们用一个简单的博客页面,演示如何使用 Astro。
示例#
以下是我们使用 Astro 实现的博客页面。SamplePost
页面导入了一个交互式组件,SocialButtons
。该组件通过标记确定在 HTML 中的具体位置。
Astro page (SamplePost.astro)
--- // Component Imports import { SocialButtons } from '../../components/SocialButtons.tsx'; --- <html lang="en"> <head> <link rel="stylesheet" href="/blog.css" /> </head> <body> <div class="layout"> <article class="content"> <section class="intro"> <h1 class="title">Post title (static)</h1> <br /> <p>Post sub-title (static)</p> </section> <section class="intro"> <p>This is the post content with images that is rendered by the server.</p> <img src="https://source.unsplash.com/user/c_v_r/200x200" /> <p> The next section contains the interactive social buttons component which includes its script. </p> </section> <section class="social"> <div> <SocialButtons client:visible></SocialButtons> </div> </section> </article> </div> </body> </html>
SocialButtons
是一个 Preact 组件,包含 HTML 和相应的事件处理逻辑。
SocialButtons component (SocialButtons.tsx)
import { useState } from 'preact/hooks' /** a counter written in Preact */ export function SocialButtons() { const [count, setCount] = useState(0) const add = () => setCount((i) => i + 1) const subtract = () => setCount((i) => i - 1) return ( <> <div>{count} people liked this post</div> <div align="right"> <img src="/like.png" width="32" height="32" onclick={add}></img> <img src="/unlike.png" width="32" height="32" onclick={subtract}></img> </div> </> ) }
该组件在运行时嵌入到页面中,并在客户端进行 hydrate,以支持 click 事件。
Astro 允许在 HTML、CSS 和 JS 之间进行清晰的隔离,并鼓励基于组件的设计。使用此框架可以轻松构建网站。
优点和缺点#
Islands Architecture 综合了不同渲染技术的 Idea,例如服务端渲染、静态站点生成和部分 hydrate。Islands 的一些潜在好处是:
- 性能:降低客户端 JS 代码的体积。加载的代码,仅包含交互式组件所需的 JS,这比为整个页面重新创建 Virtual DOM,并重新 hydrate 页面上的所有元素所需的 JS 要少得多。较小的 JS 会带来更快的页面加载和交互时间 (TTI)。
Astro 与使用 Next.js 和 Nuxt.js 创建的文档网站相比,JS 代码减少了 83%。其他用户也反馈了使用 Astro 的性能改进。
- SEO:由于所有静态内容经过服务端渲染,所以页面对 SEO 场景是友好的
- 关键内容优先:用户几乎可以立即获得关键内容(尤其是博客、新闻文章和产品类的页面)。在关键内容逐渐可用后,可交互的次要功能也是必须的。
- 可访问性:使用标准静态 HTML 链接导航,有助于提高网站的可访问性(Accessibility)。
- 基于组件:该架构具有基于组件的架构的所有优点,如:可重用、可维护。
尽管有这些优势,但该概念仍处于初期阶段。“有限的支持”导致了一些缺点:
- 开发人员实现 Islands 的唯一选择,是使用少数可用的框架之一,或自己实现架构。将现有站点迁移到 Astro 或 Marko 需要额外的工作量。
- 除了 Jason 最初的文章外,几乎没有更多的信息。
- 有一些新框架声称支持 Islands Architecture,因此筛选出适合你的架构会更难。
- 该架构不适合复杂交互页面,例如可能需要数千个 Islands 的社交应用。
Islands Architecture 的概念相对较新,但由于其性能优势,可能会发展很快。它强调了使用 SSR 渲染静态内容,同时通过动态组件来支持交互性,并且对页面性能的影响最小。希望将来在这个领域看到更多的参与者,并有更广泛的框架可供选择。
进一步阅读#
- Islands Architecture(原文 | 译文)
- Is 0KB of JavaScript in your future Modernizing Etsy’s codebase with React