rendering-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Web Rendering Patterns

Web渲染模式

Overview

概述

Rendering determines when and where HTML is generated. Each pattern has distinct performance, SEO, and infrastructure implications.
渲染决定了HTML的生成时机地点。每种渲染模式在性能、SEO(搜索引擎优化)以及基础设施方面都有不同的影响。

Rendering Pattern Summary

渲染模式汇总

PatternWhen GeneratedWhere GeneratedUse Case
CSRRuntime (browser)ClientDashboards, authenticated apps
SSRRuntime (each request)ServerDynamic, personalized content
SSGBuild timeServer/BuildStatic content, blogs, docs
ISRBuild + revalidationServerContent that changes periodically
StreamingRuntime (progressive)ServerLong pages, slow data sources
模式生成时机生成地点适用场景
CSR运行时(浏览器端)客户端仪表盘、需鉴权的应用
SSR运行时(每次请求)服务端动态、个性化内容
SSG构建时服务端/构建阶段静态内容、博客、文档
ISR构建时 + 后台再生服务端定期更新的内容
流式渲染运行时(渐进式)服务端长页面、慢数据源场景

Client-Side Rendering (CSR)

客户端渲染(CSR)

Browser generates HTML using JavaScript after page load.
页面加载完成后,由浏览器通过JavaScript生成HTML。

Flow

流程

1. Browser requests page
2. Server returns minimal HTML shell + JS bundle
3. JS executes in browser
4. JS fetches data from API
5. JS renders HTML into DOM
1. 浏览器请求页面
2. 服务端返回极简HTML外壳 + JS包
3. 浏览器执行JS
4. JS从API获取数据
5. JS将HTML渲染到DOM中

Timeline

时间线

Request → Empty Shell → JS Downloads → JS Executes → Data Fetches → Content Visible
         [--------------- Time to Interactive (slow) ---------------]
请求 → 空白外壳 → JS下载 → JS执行 → 数据获取 → 内容可见
         [--------------- 交互时间(较慢) ---------------]

Characteristics

特性

  • First Contentful Paint (FCP): Slow (waiting for JS)
  • Time to Interactive (TTI): Slow (JS must execute + fetch data)
  • SEO: Poor (crawlers see empty shell unless they execute JS)
  • Server load: Low (static files only)
  • Caching: Easy (static assets)
  • 首次内容绘制(FCP):较慢(需等待JS执行)
  • 交互时间(TTI):较慢(JS需执行并获取数据)
  • SEO:较差(爬虫看到的是空白外壳,除非执行JS)
  • 服务端负载:低(仅返回静态文件)
  • 缓存:简单(静态资源易缓存)

When to Use

适用场景

  • Authenticated dashboards (no SEO needed)
  • Highly interactive apps
  • Real-time data that can't be pre-rendered
  • When server infrastructure is limited
  • 需鉴权的仪表盘(无需SEO)
  • 高交互性应用
  • 无法预渲染的实时数据场景
  • 服务端基础设施有限时

Code Pattern

代码示例

jsx
// Pure CSR - data fetched in browser
function ProductPage() {
  const [product, setProduct] = useState(null);
  
  useEffect(() => {
    fetch('/api/products/123')
      .then(res => res.json())
      .then(setProduct);
  }, []);
  
  if (!product) return <Loading />;
  return <Product data={product} />;
}
jsx
// 纯CSR - 浏览器端获取数据
function ProductPage() {
  const [product, setProduct] = useState(null);
  
  useEffect(() => {
    fetch('/api/products/123')
      .then(res => res.json())
      .then(setProduct);
  }, []);
  
  if (!product) return <Loading />;
  return <Product data={product} />;
}

Server-Side Rendering (SSR)

服务端渲染(SSR)

Server generates complete HTML for each request.
服务端为每个请求生成完整的HTML。

Flow

流程

1. Browser requests page
2. Server fetches data
3. Server renders HTML with data
4. Server sends complete HTML
5. Browser displays content immediately
6. JS hydrates for interactivity
1. 浏览器请求页面
2. 服务端获取数据
3. 服务端结合数据渲染HTML
4. 服务端返回完整HTML
5. 浏览器立即显示内容
6. JS进行水合以实现交互性

Timeline

时间线

Request → Server Fetches Data → Server Renders → HTML Sent → Content Visible → Hydration → Interactive
          [--- Server Time ---]                              [--- Hydration ---]
请求 → 服务端获取数据 → 服务端渲染 → HTML返回 → 内容可见 → 水合 → 可交互
          [--- 服务端处理时间 ---]                              [--- 水合阶段 ---]

Characteristics

特性

  • FCP: Fast (complete HTML from server)
  • TTI: Depends on hydration time
  • SEO: Excellent (full HTML for crawlers)
  • Server load: High (render on every request)
  • Caching: Complex (varies by user/request)
  • FCP:较快(服务端返回完整HTML)
  • TTI:取决于水合时间
  • SEO:优秀(爬虫可获取完整HTML)
  • 服务端负载:高(每次请求都要渲染)
  • 缓存:复杂(因用户/请求而异)

When to Use

适用场景

  • SEO-critical pages with dynamic content
  • Personalized content (user-specific)
  • Frequently changing data
  • Pages that need fresh data on every request
  • 对SEO要求高且内容动态的页面
  • 个性化内容(用户专属)
  • 频繁更新的数据
  • 每次请求都需要新鲜数据的页面

Code Pattern (Framework-agnostic concept)

代码示例(框架无关概念)

javascript
// Server-side: runs on each request
async function renderPage(request) {
  const data = await fetchData(request.params.id);
  const html = renderToString(<Page data={data} />);
  return html;
}
javascript
// 服务端代码:每次请求时运行
async function renderPage(request) {
  const data = await fetchData(request.params.id);
  const html = renderToString(<Page data={data} />);
  return html;
}

Static Site Generation (SSG)

静态站点生成(SSG)

HTML generated once at build time.
HTML在构建阶段一次性生成。

Flow

流程

Build Time:
1. Build process fetches all data
2. Generates HTML for all pages
3. Outputs static files

Runtime:
1. Browser requests page
2. CDN serves pre-built HTML instantly
构建阶段:
1. 构建过程获取所有数据
2. 为所有页面生成HTML
3. 输出静态文件

运行时:
1. 浏览器请求页面
2. CDN立即返回预构建的HTML

Timeline

时间线

Request → CDN Cache Hit → HTML Delivered → Content Visible (instant)
请求 → CDN缓存命中 → HTML交付 → 内容立即可见

Characteristics

特性

  • FCP: Fastest (pre-built, CDN-cached)
  • TTI: Fast (minimal JS, or none)
  • SEO: Excellent (complete HTML)
  • Server load: None at runtime (static files)
  • Caching: Trivial (immutable until next build)
  • FCP:最快(预构建且CDN缓存)
  • TTI:较快(JS极少或无)
  • SEO:优秀(完整HTML)
  • 服务端负载:运行时无负载(仅静态文件)
  • 缓存:极易(下次构建前内容不变)

When to Use

适用场景

  • Blogs, documentation, marketing pages
  • Content that changes infrequently
  • Pages where all possible URLs are known at build time
  • Maximum performance is required
  • 博客、文档、营销页面
  • 内容更新频率低的场景
  • 所有可能的URL在构建时已知的页面
  • 对性能要求极高的场景

Limitations

局限性

  • Content stale until rebuild
  • Build time grows with page count
  • Can't handle dynamic routes unknown at build time
  • Personalization requires client-side hydration
  • 内容在下次构建前会过时
  • 构建时间随页面数量增加而变长
  • 无法处理构建时未知的动态路由
  • 个性化内容需要客户端水合

Incremental Static Regeneration (ISR)

增量静态再生(ISR)

SSG with automatic background regeneration.
带自动后台再生功能的SSG。

Flow

流程

1. Initial build generates static pages
2. Pages served from cache
3. After revalidation time expires:
   - Serve stale page (fast)
   - Regenerate in background
   - Next request gets fresh page
1. 初始构建生成静态页面
2. 从缓存中返回页面
3. 超过再生时间后:
   - 立即返回旧页面(快速)
   - 在后台重新生成页面
   - 下一次请求将获取新页面

Revalidation Strategies

再生策略

Time-based:
Page generated → Serve for 60 seconds → Regenerate on next request after 60s
On-demand:
CMS publishes → Webhook triggers regeneration → Page updated immediately
基于时间:
页面生成 → 提供60秒 → 60秒后下一次请求时重新生成
按需触发:
CMS发布内容 → Webhook触发再生 → 页面立即更新

Characteristics

特性

  • FCP: Fast (cached HTML)
  • Freshness: Configurable (seconds to hours)
  • SEO: Excellent
  • Server load: Low (regenerate occasionally)
  • Scalability: High (mostly static)
  • FCP:较快(缓存的HTML)
  • 新鲜度:可配置(从秒到小时)
  • SEO:优秀
  • 服务端负载:低(仅偶尔再生)
  • 可扩展性:高(大部分为静态内容)

When to Use

适用场景

  • E-commerce product pages
  • News sites
  • Content that updates but not real-time
  • High-traffic pages that need freshness
  • 电商商品页面
  • 新闻站点
  • 内容会更新但非实时的场景
  • 高流量且需要内容新鲜度的页面

Streaming / Progressive Rendering

流式渲染 / 渐进式渲染

Server sends HTML in chunks as data becomes available.
服务端在数据就绪时分块发送HTML。

Flow

流程

1. Browser requests page
2. Server immediately sends HTML shell
3. Server fetches data (possibly in parallel)
4. Server streams HTML chunks as ready
5. Browser renders progressively
1. 浏览器请求页面
2. 服务端立即发送HTML外壳
3. 服务端获取数据(可并行)
4. 数据就绪时,服务端流式发送HTML块
5. 浏览器渐进式渲染内容

Timeline

时间线

Request → Shell Sent → [Chunk 1 Streams] → [Chunk 2 Streams] → Complete
          [Visible]    [More Visible]      [Fully Visible]
请求 → 外壳返回 → [块1流式传输] → [块2流式传输] → 完成
          [内容可见]    [更多内容可见]      [内容完全可见]

Characteristics

特性

  • FCP: Very fast (shell immediate)
  • TTFB: Fast (no waiting for all data)
  • User Experience: Progressive disclosure
  • Complexity: Higher implementation
  • FCP:极快(立即返回外壳)
  • 首次字节时间(TTFB):较快(无需等待所有数据)
  • 用户体验:渐进式内容展示
  • 复杂度:实现难度较高

When to Use

适用场景

  • Pages with multiple data sources
  • Slow API dependencies
  • Long pages where top should render first
  • Improving perceived performance
  • 多数据源的页面
  • 依赖慢速API的场景
  • 长页面(顶部内容需优先渲染)
  • 提升感知性能的场景

Rendering Decision Flowchart

渲染策略决策流程图

Does the page need SEO?
├── No → Does it need real-time data?
│        ├── Yes → CSR
│        └── No → SSG or CSR
└── Yes → Is content the same for all users?
          ├── Yes → Does content change often?
          │         ├── Rarely → SSG
          │         ├── Sometimes → ISR
          │         └── Every request → SSR
          └── No (personalized) → SSR with caching strategies
页面需要SEO吗?
├── 不需要 → 是否需要实时数据?
│        ├── 是 → CSR
│        └── 否 → SSG或CSR
└── 需要 → 所有用户看到的内容是否相同?
          ├── 是 → 内容更新频率?
          │         ├── 极少 → SSG
          │         ├── 偶尔 → ISR
          │         └── 每次请求都变 → SSR
          └── 否(个性化) → SSR结合缓存策略

Hybrid Approaches

混合方案

Modern apps mix patterns per route:
RoutePatternReason
/
(home)
SSGStatic marketing content
/blog/*
SSG/ISRContent changes occasionally
/products/*
ISRPrices update, need SEO
/dashboard
CSRAuthenticated, no SEO
/search
SSRDynamic query results
现代应用会为不同路由混合使用多种模式:
路由模式原因
/
(首页)
SSG静态营销内容
/blog/*
SSG/ISR内容偶尔更新
/products/*
ISR价格会更新,且需要SEO
/dashboard
CSR需鉴权,无需SEO
/search
SSR动态查询结果

Performance Metrics Impact

对性能指标的影响

PatternTTFBFCPLCPTTI
CSRFastSlowSlowSlow
SSRSlowerFastFastMedium
SSGFastestFastestFastestFast
ISRFastestFastestFastestFast
StreamingFastFastProgressiveMedium

模式TTFBFCPLCPTTI
CSR
SSR较慢中等
SSG最快最快最快
ISR最快最快最快
流式渲染渐进式中等

Deep Dive: Understanding How Rendering Actually Works

深入理解:渲染的底层原理

What Does "Rendering" Actually Mean?

「渲染」到底指什么?

Rendering is the process of converting your application code into HTML that a browser can display. Understanding this deeply requires knowing what happens at each layer.
The Rendering Pipeline:
Your Code (JSX, Vue template, Svelte)
Framework Virtual Representation (Virtual DOM, reactive graph)
HTML String or DOM Operations
Browser's DOM Tree
Browser's Render Tree (DOM + CSS)
Layout (position, size calculations)
Paint (pixels on screen)
Composite (layers combined)
When we say "server rendering" vs "client rendering", we're talking about WHERE the first few steps happen.
渲染是将应用代码转换为浏览器可显示的HTML的过程。要深入理解这一点,需要了解每个环节的工作。
渲染流水线:
应用代码(JSX、Vue模板、Svelte)
框架虚拟表示(虚拟DOM、响应式图)
HTML字符串或DOM操作
浏览器DOM树
浏览器渲染树(DOM + CSS)
布局(位置、尺寸计算)
绘制(屏幕像素渲染)
合成(图层合并)
当我们说「服务端渲染」和「客户端渲染」时,指的是前几个步骤发生的位置不同。

The Fundamental Trade-off: Work Location

核心权衡:工作负载的分配

Every web page requires work to be done. The question is: who does the work?
SERVER RENDERING:
┌─────────────────────────────────────────────────────────────────┐
│ SERVER (powerful, shared)                                       │
│                                                                 │
│  [Fetch Data] → [Build HTML String] → [Send HTML]              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT (varies, individual)                                     │
│                                                                 │
│  [Parse HTML] → [Build DOM] → [Display]                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘


CLIENT RENDERING:
┌─────────────────────────────────────────────────────────────────┐
│ SERVER (powerful, shared)                                       │
│                                                                 │
│  [Send static JS bundle]                                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT (varies, individual)                                     │
│                                                                 │
│  [Download JS] → [Execute JS] → [Fetch Data] → [Build DOM]     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
Server rendering: Server does more work, client does less Client rendering: Server does less work, client does more
每个网页都需要完成一定的工作,问题在于:由谁来做这些工作?
服务端渲染:
┌─────────────────────────────────────────────────────────────────┐
│ 服务端(性能强,共享资源)                                       │
│                                                                 │
│  [获取数据] → [生成HTML字符串] → [返回HTML]                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 客户端(性能各异,独立设备)                                     │
│                                                                 │
│  [解析HTML] → [构建DOM] → [显示内容]                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘


客户端渲染:
┌─────────────────────────────────────────────────────────────────┐
│ 服务端(性能强,共享资源)                                       │
│                                                                 │
│  [返回静态JS包]                                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 客户端(性能各异,独立设备)                                     │
│                                                                 │
│  [下载JS] → [执行JS] → [获取数据] → [构建DOM]     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
服务端渲染:服务端承担更多工作,客户端工作更少 客户端渲染:服务端工作更少,客户端承担更多工作

How Server-Side Rendering Works Internally

服务端渲染的内部工作原理

When a server renders HTML, it runs your component code to produce a string:
javascript
// What your React component looks like
function ProductPage({ product }) {
  return (
    <div className="product">
      <h1>{product.name}</h1>
      <p>${product.price}</p>
    </div>
  );
}

// What the server actually does (simplified)
function renderToString(component) {
  // 1. Execute component function
  const virtualDOM = component({ product: { name: 'Shoes', price: 99 } });
  
  // 2. virtualDOM is a tree structure:
  // {
  //   type: 'div',
  //   props: { className: 'product' },
  //   children: [
  //     { type: 'h1', children: ['Shoes'] },
  //     { type: 'p', children: ['$99'] }
  //   ]
  // }
  
  // 3. Convert tree to HTML string
  return '<div class="product"><h1>Shoes</h1><p>$99</p></div>';
}
The server runs JavaScript to produce HTML. It's executing your React/Vue/Svelte code, but instead of updating a browser DOM, it builds a string.
服务端渲染HTML时,会运行组件代码以生成字符串:
javascript
// React组件示例
function ProductPage({ product }) {
  return (
    <div className="product">
      <h1>{product.name}</h1>
      <p>${product.price}</p>
    </div>
  );
}

// 服务端的简化操作
function renderToString(component) {
  // 1. 执行组件函数
  const virtualDOM = component({ product: { name: 'Shoes', price: 99 } });
  
  // 2. 虚拟DOM是树形结构:
  // {
  //   type: 'div',
  //   props: { className: 'product' },
  //   children: [
  //     { type: 'h1', children: ['Shoes'] },
  //     { type: 'p', children: ['$99'] }
  //   ]
  // }
  
  // 3. 将树形结构转换为HTML字符串
  return '<div class="product"><h1>Shoes</h1><p>$99</p></div>';
}
服务端运行JavaScript生成HTML,它执行React/Vue/Svelte代码,但不是更新浏览器DOM,而是构建字符串。

How Client-Side Rendering Works Internally

客户端渲染的内部工作原理

CSR works differently - it manipulates the live DOM:
javascript
// Browser receives empty HTML:
// <div id="root"></div>

// JavaScript bundle executes:
const root = document.getElementById('root');

// Framework creates elements and appends them
const div = document.createElement('div');
div.className = 'product';

const h1 = document.createElement('h1');
h1.textContent = 'Shoes';

const p = document.createElement('p');
p.textContent = '$99';

div.appendChild(h1);
div.appendChild(p);
root.appendChild(div);

// Each DOM operation triggers browser work
Every DOM manipulation can trigger layout and paint. This is why virtual DOM exists - to batch changes.
CSR的工作方式不同——它操作实时DOM:
javascript
// 浏览器收到的空白HTML:
// <div id="root"></div>

// JS包执行:
const root = document.getElementById('root');

// 框架创建元素并追加
const div = document.createElement('div');
div.className = 'product';

const h1 = document.createElement('h1');
h1.textContent = 'Shoes';

const p = document.createElement('p');
p.textContent = '$99';

div.appendChild(h1);
div.appendChild(p);
root.appendChild(div);

// 每个DOM操作都会触发浏览器的工作
每次DOM操作都会触发布局和绘制。这就是虚拟DOM存在的原因——批量处理变更。

Why SSG is Fastest: CDN Edge Caching

为什么SSG最快:CDN边缘缓存

Static Site Generation's speed comes from CDN distribution:
Traditional SSR:
User (Tokyo) → Request → Origin Server (New York) → Process → Response
              [───────────── 200-500ms ──────────────]

Static + CDN:
User (Tokyo) → Request → CDN Edge (Tokyo) → Cache Hit → Response
              [──────────── 20-50ms ────────────]
How CDNs Work:
First Request (cache miss):
1. User requests /products
2. CDN edge has no cache
3. Request goes to origin
4. Origin returns HTML
5. CDN caches it
6. User receives response

Subsequent Requests (cache hit):
1. User requests /products
2. CDN edge has cached HTML
3. Immediate response (no origin contact)
With SSG, the HTML is pre-built and distributed to CDN edges worldwide BEFORE any user requests it.
静态站点生成的速度来自CDN分发:
传统SSR:
用户(东京) → 请求 → 源服务端(纽约) → 处理 → 响应
              [───────────── 200-500ms ──────────────]

静态+CDN:
用户(东京) → 请求 → CDN边缘节点(东京) → 缓存命中 → 响应
              [──────────── 20-50ms ────────────]
CDN工作原理:
首次请求(缓存未命中):
1. 用户请求 /products
2. CDN边缘节点无缓存
3. 请求转发到源服务端
4. 源服务端返回HTML
5. CDN缓存该HTML
6. 用户收到响应

后续请求(缓存命中):
1. 用户请求 /products
2. CDN边缘节点有缓存的HTML
3. 立即返回响应(无需联系源服务端)
SSG会在用户请求之前,预先构建HTML并分发到全球各地的CDN边缘节点。

ISR: How Stale-While-Revalidate Works

ISR:「旧内容可用时再生」的工作原理

ISR combines caching with freshness. Understanding the timeline is key:
Build Time: Page generated, cached with revalidate: 60

Timeline:
[0s]     Page built, served to all users
[30s]    User A requests → Gets cached page (age: 30s, still fresh)
[60s]    Page is now "stale" but still cached
[61s]    User B requests → Gets stale page immediately
         Background: Server regenerates page
[62s]    New page ready, replaces old in cache
[70s]    User C requests → Gets fresh page (age: 8s)
The key insight: the user who triggers revalidation gets the stale page. The NEXT user gets fresh content. This is "stale-while-revalidate" strategy.
ISR结合了缓存与新鲜度,理解时间线是关键:
构建阶段:页面生成,缓存再生时间设为60秒

时间线:
[0s]     页面构建完成,提供给所有用户
[30s]    用户A请求 → 获取缓存页面(已缓存30秒,仍为新鲜内容)
[60s]    页面变为「旧内容」但仍在缓存中
[61s]    用户B请求 → 立即获取旧内容
         后台:服务端重新生成页面
[62s]    新页面就绪,替换缓存中的旧页面
[70s]    用户C请求 → 获取新鲜页面(已缓存8秒)
核心要点:触发再生的用户看到的是旧内容,下一个用户会获取新鲜内容。这就是「旧内容可用时再生」策略。

Streaming: How HTTP Chunked Transfer Works

流式渲染:HTTP分块传输的工作原理

Streaming isn't magic - it uses HTTP's chunked transfer encoding:
Normal Response:
HTTP/1.1 200 OK
Content-Length: 5000

[...waits until all 5000 bytes ready...]
[...sends all at once...]


Chunked/Streaming Response:
HTTP/1.1 200 OK
Transfer-Encoding: chunked

100                          ← Chunk size in hex (256 bytes)
<html><head>...</head><body> ← Actual content
0                            ← More chunks coming

200                          ← Next chunk (512 bytes)
<main>Content here...</main>
0

0                            ← Final chunk (0 means done)
The browser can start rendering BEFORE the full response arrives.
React Streaming Example:
jsx
// Server Component
async function Page() {
  return (
    <html>
      <body>
        <Header />  {/* Immediate - no data */}
        
        <Suspense fallback={<LoadingSkeleton />}>
          <SlowDataComponent />  {/* Streams when ready */}
        </Suspense>
        
        <Footer />  {/* Immediate - no data */}
      </body>
    </html>
  );
}

// What gets streamed:
// Chunk 1: <html><body><Header>...</Header><LoadingSkeleton />
// [... server fetching SlowDataComponent data ...]
// Chunk 2: <script>swapContent('slow-component', '<SlowData>...</SlowData>')</script>
// Chunk 3: <Footer>...</Footer></body></html>
流式渲染并非魔法——它使用HTTP的分块传输编码:
普通响应:
HTTP/1.1 200 OK
Content-Length: 5000

[...等待所有5000字节准备好...]
[...一次性发送...]


分块/流式响应:
HTTP/1.1 200 OK
Transfer-Encoding: chunked

100                          ← 十六进制表示的块大小(256字节)
<html><head>...</head><body> ← 实际内容
0                            ← 还有更多块

200                          ← 下一个块(512字节)
<main>Content here...</main>
0

0                            ← 最终块(0表示完成)
浏览器可以在收到完整响应前开始渲染。
React流式渲染示例:
jsx
// 服务端组件
async function Page() {
  return (
    <html>
      <body>
        <Header />  {/* 立即渲染 - 无数据依赖 */}
        
        <Suspense fallback={<LoadingSkeleton />}>
          <SlowDataComponent />  {/* 数据就绪时流式传输 */}
        </Suspense>
        
        <Footer />  {/* 立即渲染 - 无数据依赖 */}
      </body>
    </html>
  );
}

// 流式传输内容:
// 块1: <html><body><Header>...</Header><LoadingSkeleton />
// [... 服务端获取SlowDataComponent数据 ...]
// 块2: <script>swapContent('slow-component', '<SlowData>...</SlowData>')</script>
// 块3: <Footer>...</Footer></body></html>

Time to First Byte (TTFB) vs Time to Last Byte

首次字节时间(TTFB) vs 最后字节时间

Understanding these metrics clarifies rendering tradeoffs:
CSR Request:
[Request]─────[TTFB: 50ms]─────[TTLB: 100ms]
              Small static file, fast to send

SSR Request (blocking):
[Request]─────────────────────[TTFB: 500ms]─────[TTLB: 520ms]
              Server processing delays first byte

SSR Request (streaming):
[Request]───[TTFB: 50ms]─────────────────────────[TTLB: 500ms]
            Shell immediate      Content streams progressively
Streaming improves TTFB dramatically while allowing complex server work.
理解这些指标能明确渲染的权衡:
CSR请求:
[请求]─────[TTFB: 50ms]─────[TTLB: 100ms]
              静态文件体积小,发送快

SSR请求(阻塞式):
[Request]─────────────────────[TTFB: 500ms]─────[TTLB: 520ms]
              服务端处理延迟了首次字节的返回

SSR请求(流式):
[Request]───[TTFB: 50ms]─────────────────────────[TTLB: 500ms]
            立即返回外壳      内容渐进式流式传输
流式传输显著提升了TTFB,同时允许服务端处理复杂工作。

The N+1 Problem in Rendering

渲染中的N+1问题

Both SSR and CSR can suffer from N+1 data fetching:
javascript
// Waterfall problem
async function ProductsPage() {
  const products = await fetch('/api/products');  // 1 request
  
  return products.map(product => (
    <Product 
      product={product}
      reviews={await fetch(`/api/reviews/${product.id}`)}  // N requests
    />
  ));
}

// If you have 50 products = 51 requests (serial!)
// Each awaits the previous = massive latency
Solution - Parallel fetching:
javascript
async function ProductsPage() {
  const products = await fetch('/api/products');
  
  // Fetch all reviews in parallel
  const reviewsPromises = products.map(p => 
    fetch(`/api/reviews/${p.id}`)
  );
  const reviews = await Promise.all(reviewsPromises);
  
  // Now render with all data
}
SSR和CSR都可能遇到N+1数据获取问题:
javascript
// 瀑布流问题
async function ProductsPage() {
  const products = await fetch('/api/products');  // 1次请求
  
  return products.map(product => (
    <Product 
      product={product}
      reviews={await fetch(`/api/reviews/${product.id}`)}  // N次请求
    />
  ));
}

// 如果有50个产品 = 51次请求(串行!)
// 每次请求都等待前一个完成 → 延迟极高
解决方案 - 并行获取:
javascript
async function ProductsPage() {
  const products = await fetch('/api/products');
  
  // 并行获取所有评论
  const reviewsPromises = products.map(p => 
    fetch(`/api/reviews/${p.id}`)
  );
  const reviews = await Promise.all(reviewsPromises);
  
  // 现在使用所有数据渲染
}

Cache Invalidation: The Hard Problem

缓存失效:计算机科学的难题

"There are only two hard things in Computer Science: cache invalidation and naming things."
Problem Scenario:
1. Page generated with product price $99
2. Price changes to $89 in database
3. Cached page still shows $99
4. User sees wrong price

Cache Strategies:

TIME-BASED (ISR):
- Revalidate every 60 seconds
- Pros: Simple, predictable
- Cons: Up to 60s stale data

EVENT-BASED (On-demand revalidation):
- CMS webhook triggers rebuild
- Pros: Immediate freshness
- Cons: Complex, webhook reliability

CACHE TAGS:
- Tag pages by dependency: ['product-123', 'category-shoes']
- Invalidate by tag when data changes
- Pros: Precise invalidation
- Cons: Complex dependency tracking
"计算机科学中只有两件难事:缓存失效和命名。"
问题场景:
1. 页面生成时产品价格为$99
2. 数据库中价格变为$89
3. 缓存页面仍显示$99
4. 用户看到错误价格

缓存策略:

基于时间(ISR):
- 每60秒再生一次
- 优点:简单、可预测
- 缺点:最多有60秒的旧内容

基于事件(按需再生):
- CMS发布内容后,Webhook触发重建
- 优点:立即更新内容
- 缺点:实现复杂,依赖Webhook可靠性

缓存标签:
- 按依赖项为页面打标签:['product-123', 'category-shoes']
- 数据变更时按标签失效缓存
- 优点:精确失效
- 缺点:依赖项追踪复杂

Edge Rendering vs Origin Rendering

边缘渲染 vs 源服务端渲染

Modern architectures introduce edge computing:
ORIGIN RENDERING (traditional SSR):
User → CDN Edge → Origin Server (single location) → Response
       [20ms]     [─────── 200ms ───────]

EDGE RENDERING:
User → CDN Edge (runs your code) → Response
       [──────── 50ms ───────]
       No origin round-trip needed

EDGE + ORIGIN:
User → Edge (static parts) → Origin (dynamic parts)
       [──── 50ms ────]     [─── 150ms for dynamic ───]
       Shell immediate       Data streams in
Edge functions run your server code geographically close to users. Platforms like Cloudflare Workers, Vercel Edge, Deno Deploy enable this.
现代架构引入了边缘计算:
源服务端渲染(传统SSR):
用户 → CDN边缘节点 → 源服务端(单一位置) → 响应
       [20ms]     [─────── 200ms ───────]

边缘渲染:
用户 → CDN边缘节点(运行你的代码) → 响应
       [──────── 50ms ───────]
       无需往返源服务端

边缘+源服务端:
用户 → 边缘节点(静态部分) → 源服务端(动态部分)
       [──── 50ms ────]     [─── 150ms处理动态内容 ───]
       立即返回外壳       数据流式传输
边缘函数在地理上靠近用户的位置运行服务端代码。Cloudflare Workers、Vercel Edge、Deno Deploy等平台支持这种架构。

Memory and CPU: The Rendering Costs

内存与CPU:渲染的成本

CSR Memory Pattern:
Browser Memory Over Time:
[Page Load] → [JS Parsed: 50MB heap] → [App Runs: 80MB] → [Navigation: 100MB] → ...
                                       Memory grows, garbage collected periodically
                                       Memory leaks possible if state not cleaned
SSR Memory Pattern:
Server Memory Per Request:
[Request] → [Render: 20MB allocated] → [Response Sent] → [Memory freed]
            Fresh start each request
            No memory leaks across requests
            But: concurrent requests = concurrent memory
CPU Considerations:
CSR: Each user's device does rendering work
     1000 users = 1000 CPUs doing work (distributed)

SSR: Server does all rendering work
     1000 users = 1 server CPU doing 1000x work (concentrated)
     
Solution: Caching (SSG/ISR) or scaling servers
CSR内存模式:
浏览器内存随时间变化:
[页面加载] → [JS解析完成: 50MB堆内存] → [应用运行: 80MB] → [页面导航: 100MB] → ...
                                       内存增长,定期垃圾回收
                                       若状态未清理可能出现内存泄漏
SSR内存模式:
服务端每请求内存占用:
[请求] → [渲染: 分配20MB] → [响应返回] → [内存释放]
            每次请求重新开始
            跨请求无内存泄漏
            但:并发请求 = 并发内存占用
CPU考量:
CSR: 每个用户的设备承担渲染工作
     1000个用户 = 1000个CPU处理工作(分布式)

SSR: 服务端承担所有渲染工作
     1000个用户 = 1个服务端CPU处理1000倍工作(集中式)
     
解决方案:缓存(SSG/ISR)或扩容服务端

When to Use What: Deep Analysis

场景选择深度分析

Choose CSR when:
  • Users are authenticated (can't cache anyway)
  • Data is real-time (WebSockets, polling)
  • Heavy interactivity (dashboards, editors)
  • Server costs must be minimal
  • SEO doesn't matter
Choose SSR when:
  • SEO is critical AND data is dynamic
  • Personalization needed (user-specific content)
  • Data changes every request
  • You can handle server costs
  • First paint must include real content
Choose SSG when:
  • Content changes rarely (docs, blogs)
  • All URLs known at build time
  • Maximum performance needed
  • Minimal server infrastructure
  • Content is same for all users
Choose ISR when:
  • Content changes but not constantly
  • SEO needed but data updates
  • Can tolerate brief staleness
  • Want SSG benefits with some dynamism
Choose Streaming when:
  • Parts of page have slow data dependencies
  • Want fast first paint with SSR
  • Page has mixed static/dynamic content
  • User experience during load matters

选择CSR的场景:
  • 用户已鉴权(无法缓存)
  • 数据是实时的(WebSocket、轮询)
  • 高交互性(仪表盘、编辑器)
  • 服务端成本必须最小化
  • SEO不重要
选择SSR的场景:
  • SEO关键且内容动态
  • 需要个性化(用户专属内容)
  • 数据每次请求都变化
  • 可承担服务端成本
  • 首次绘制必须包含真实内容
选择SSG的场景:
  • 内容极少更新(文档、博客)
  • 所有URL在构建时已知
  • 需要极致性能
  • 服务端基础设施最少
  • 所有用户看到的内容相同
选择ISR的场景:
  • 内容会更新但非频繁
  • 需要SEO但数据会更新
  • 可容忍短暂的旧内容
  • 想要SSG的优势同时具备一定动态性
选择流式渲染的场景:
  • 页面部分内容依赖慢速数据源
  • 想要SSR的快速首次绘制
  • 页面混合静态与动态内容
  • 加载时的用户体验很重要

For Framework Authors: Implementing Rendering Systems

给框架开发者:实现渲染系统

Implementation Note: The patterns and code examples below represent one proven approach to building rendering systems. There are many valid implementation strategies—the direction shown here is based on patterns from React, Preact, Solid, and other frameworks. Your implementation may differ based on your virtual DOM design, component model, and performance goals. Use these as architectural guidance rather than prescriptive solutions.
实现说明:以下模式和代码示例代表了构建渲染系统的一种成熟方案。存在多种有效的实现策略——这里展示的方向基于React、Preact、Solid等框架的模式。你的实现可能因虚拟DOM设计、组件模型和性能目标而异。请将这些作为架构指导而非规定性方案。

Building a Server-Side Renderer

构建服务端渲染器

Core SSR implementation for React-like frameworks:
javascript
// MINIMAL SSR IMPLEMENTATION

// 1. Component to HTML string conversion
function renderToString(element) {
  if (typeof element === 'string' || typeof element === 'number') {
    return escapeHtml(String(element));
  }
  
  if (element === null || element === undefined || element === false) {
    return '';
  }
  
  if (Array.isArray(element)) {
    return element.map(renderToString).join('');
  }
  
  const { type, props } = element;
  
  // Function component
  if (typeof type === 'function') {
    const result = type(props);
    return renderToString(result);
  }
  
  // HTML element
  const attributes = renderAttributes(props);
  const children = renderToString(props.children);
  
  // Void elements (no closing tag)
  if (VOID_ELEMENTS.has(type)) {
    return `<${type}${attributes}>`;
  }
  
  return `<${type}${attributes}>${children}</${type}>`;
}

function renderAttributes(props) {
  return Object.entries(props || {})
    .filter(([key]) => key !== 'children' && !key.startsWith('on'))
    .map(([key, value]) => {
      if (key === 'className') key = 'class';
      if (key === 'htmlFor') key = 'for';
      if (typeof value === 'boolean') {
        return value ? ` ${key}` : '';
      }
      return ` ${key}="${escapeHtml(String(value))}"`;
    })
    .join('');
}

// 2. Full HTML document wrapper
function renderDocument(app, { head, scripts, styles }) {
  return `<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  ${head}
  ${styles.map(s => `<link rel="stylesheet" href="${s}">`).join('\n')}
</head>
<body>
  <div id="app">${app}</div>
  ${scripts.map(s => `<script src="${s}"></script>`).join('\n')}
</body>
</html>`;
}

// 3. Server handler
async function handleSSR(request) {
  const url = new URL(request.url);
  const route = matchRoute(url.pathname);
  
  // Fetch data on server
  const data = await route.loader?.({ request, params: route.params });
  
  // Render component tree
  const app = renderToString(
    createElement(route.component, { data })
  );
  
  // Serialize data for hydration
  const serializedData = `<script>window.__DATA__=${JSON.stringify(data)}</script>`;
  
  const html = renderDocument(app, {
    head: renderHead(route),
    scripts: ['/client.js'],
    styles: ['/styles.css'],
  }) + serializedData;
  
  return new Response(html, {
    headers: { 'Content-Type': 'text/html' },
  });
}
类React框架的核心SSR实现:
javascript
// 极简SSR实现

// 1. 组件转HTML字符串
function renderToString(element) {
  if (typeof element === 'string' || typeof element === 'number') {
    return escapeHtml(String(element));
  }
  
  if (element === null || element === undefined || element === false) {
    return '';
  }
  
  if (Array.isArray(element)) {
    return element.map(renderToString).join('');
  }
  
  const { type, props } = element;
  
  // 函数组件
  if (typeof type === 'function') {
    const result = type(props);
    return renderToString(result);
  }
  
  // HTML元素
  const attributes = renderAttributes(props);
  const children = renderToString(props.children);
  
  // 空元素(无闭合标签)
  if (VOID_ELEMENTS.has(type)) {
    return `<${type}${attributes}>`;
  }
  
  return `<${type}${attributes}>${children}</${type}>`;
}

function renderAttributes(props) {
  return Object.entries(props || {})
    .filter(([key]) => key !== 'children' && !key.startsWith('on'))
    .map(([key, value]) => {
      if (key === 'className') key = 'class';
      if (key === 'htmlFor') key = 'for';
      if (typeof value === 'boolean') {
        return value ? ` ${key}` : '';
      }
      return ` ${key}="${escapeHtml(String(value))}"`;
    })
    .join('');
}

// 2. 完整HTML文档包装
function renderDocument(app, { head, scripts, styles }) {
  return `<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  ${head}
  ${styles.map(s => `<link rel="stylesheet" href="${s}">`).join('\n')}
</head>
<body>
  <div id="app">${app}</div>
  ${scripts.map(s => `<script src="${s}"></script>`).join('\n')}
</body>
</html>`;
}

// 3. 服务端处理器
async function handleSSR(request) {
  const url = new URL(request.url);
  const route = matchRoute(url.pathname);
  
  // 服务端获取数据
  const data = await route.loader?.({ request, params: route.params });
  
  // 渲染组件树
  const app = renderToString(
    createElement(route.component, { data })
  );
  
  // 序列化数据用于水合
  const serializedData = `<script>window.__DATA__=${JSON.stringify(data)}</script>`;
  
  const html = renderDocument(app, {
    head: renderHead(route),
    scripts: ['/client.js'],
    styles: ['/styles.css'],
  }) + serializedData;
  
  return new Response(html, {
    headers: { 'Content-Type': 'text/html' },
  });
}

Implementing Streaming SSR

实现流式SSR

javascript
// STREAMING SSR IMPLEMENTATION

async function* renderToStream(element, context) {
  // Yield shell immediately
  yield '<!DOCTYPE html><html><head></head><body><div id="app">';
  
  // Render with suspense boundaries
  yield* renderWithSuspense(element, context);
  
  // Yield closing tags
  yield '</div></body></html>';
}

async function* renderWithSuspense(element, context) {
  if (element.type === Suspense) {
    const suspenseId = context.nextSuspenseId++;
    
    // Yield fallback immediately
    yield `<template id="S:${suspenseId}">`;
    yield renderToString(element.props.fallback);
    yield `</template>`;
    
    // Start async work
    context.pendingBoundaries.push({
      id: suspenseId,
      promise: renderChildren(element.props.children, context),
    });
    
    return;
  }
  
  // Regular element - render synchronously
  yield renderToString(element);
}

// Stream pending boundaries as they complete
async function* flushPendingBoundaries(context) {
  while (context.pendingBoundaries.length > 0) {
    const completed = await Promise.race(
      context.pendingBoundaries.map(b => 
        b.promise.then(html => ({ id: b.id, html }))
      )
    );
    
    // Remove from pending
    context.pendingBoundaries = context.pendingBoundaries.filter(
      b => b.id !== completed.id
    );
    
    // Yield swap script
    yield `<script>
      const template = document.getElementById('S:${completed.id}');
      const content = document.createElement('div');
      content.innerHTML = ${JSON.stringify(completed.html)};
      template.replaceWith(content.firstChild);
    </script>`;
  }
}

// HTTP handler with streaming
async function handleStreamingSSR(request) {
  const stream = new ReadableStream({
    async start(controller) {
      const context = {
        nextSuspenseId: 0,
        pendingBoundaries: [],
      };
      
      // Stream initial content
      for await (const chunk of renderToStream(<App />, context)) {
        controller.enqueue(new TextEncoder().encode(chunk));
      }
      
      // Stream pending boundaries
      for await (const chunk of flushPendingBoundaries(context)) {
        controller.enqueue(new TextEncoder().encode(chunk));
      }
      
      controller.close();
    },
  });
  
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/html',
      'Transfer-Encoding': 'chunked',
    },
  });
}
javascript
// 流式SSR实现

async function* renderToStream(element, context) {
  // 立即返回外壳
  yield '<!DOCTYPE html><html><head></head><body><div id="app">';
  
  // 带Suspense边界渲染
  yield* renderWithSuspense(element, context);
  
  // 返回闭合标签
  yield '</div></body></html>';
}

async function* renderWithSuspense(element, context) {
  if (element.type === Suspense) {
    const suspenseId = context.nextSuspenseId++;
    
    // 立即返回fallback
    yield `<template id="S:${suspenseId}">`;
    yield renderToString(element.props.fallback);
    yield `</template>`;
    
    // 启动异步工作
    context.pendingBoundaries.push({
      id: suspenseId,
      promise: renderChildren(element.props.children, context),
    });
    
    return;
  }
  
  // 普通元素 - 同步渲染
  yield renderToString(element);
}

// 待处理边界完成后流式返回
async function* flushPendingBoundaries(context) {
  while (context.pendingBoundaries.length > 0) {
    const completed = await Promise.race(
      context.pendingBoundaries.map(b => 
        b.promise.then(html => ({ id: b.id, html }))
      )
    );
    
    // 从未处理列表中移除
    context.pendingBoundaries = context.pendingBoundaries.filter(
      b => b.id !== completed.id
    );
    
    // 返回替换脚本
    yield `<script>
      const template = document.getElementById('S:${completed.id}');
      const content = document.createElement('div');
      content.innerHTML = ${JSON.stringify(completed.html)};
      template.replaceWith(content.firstChild);
    </script>`;
  }
}

// 带流式传输的HTTP处理器
async function handleStreamingSSR(request) {
  const stream = new ReadableStream({
    async start(controller) {
      const context = {
        nextSuspenseId: 0,
        pendingBoundaries: [],
      };
      
      // 流式传输初始内容
      for await (const chunk of renderToStream(<App />, context)) {
        controller.enqueue(new TextEncoder().encode(chunk));
      }
      
      // 流式传输待处理边界
      for await (const chunk of flushPendingBoundaries(context)) {
        controller.enqueue(new TextEncoder().encode(chunk));
      }
      
      controller.close();
    },
  });
  
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/html',
      'Transfer-Encoding': 'chunked',
    },
  });
}

Building a Static Site Generator

构建静态站点生成器

javascript
// STATIC SITE GENERATOR IMPLEMENTATION

async function buildStaticSite(config) {
  const routes = await discoverRoutes(config.pagesDir);
  const outputDir = config.outputDir;
  
  // Parallel build with concurrency limit
  const limit = pLimit(10);
  
  await Promise.all(
    routes.map(route => 
      limit(async () => {
        console.log(`Building ${route.path}...`);
        
        // For dynamic routes, get all possible values
        if (route.isDynamic) {
          const paths = await route.getStaticPaths();
          await Promise.all(
            paths.map(p => buildPage(route, p, outputDir))
          );
        } else {
          await buildPage(route, {}, outputDir);
        }
      })
    )
  );
  
  // Copy static assets
  await copyDir(config.publicDir, outputDir);
  
  // Generate sitemap
  await generateSitemap(routes, outputDir);
}

async function buildPage(route, params, outputDir) {
  // Fetch data at build time
  const data = await route.loader?.({ params });
  
  // Render to string
  const html = renderToString(
    createElement(route.component, { data, params })
  );
  
  // Wrap in document
  const fullHtml = renderDocument(html, {
    head: renderHead(route, data),
    scripts: route.hasInteractivity ? ['/hydrate.js'] : [],
    styles: ['/styles.css'],
  });
  
  // Write to file
  const filePath = path.join(outputDir, route.path, 'index.html');
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, fullHtml);
}
javascript
// 静态站点生成器实现

async function buildStaticSite(config) {
  const routes = await discoverRoutes(config.pagesDir);
  const outputDir = config.outputDir;
  
  // 带并发限制的并行构建
  const limit = pLimit(10);
  
  await Promise.all(
    routes.map(route => 
      limit(async () => {
        console.log(`构建 ${route.path}...`);
        
        // 动态路由,获取所有可能的路径值
        if (route.isDynamic) {
          const paths = await route.getStaticPaths();
          await Promise.all(
            paths.map(p => buildPage(route, p, outputDir))
          );
        } else {
          await buildPage(route, {}, outputDir);
        }
      })
    )
  );
  
  // 复制静态资源
  await copyDir(config.publicDir, outputDir);
  
  // 生成站点地图
  await generateSitemap(routes, outputDir);
}

async function buildPage(route, params, outputDir) {
  // 构建时获取数据
  const data = await route.loader?.({ params });
  
  // 渲染为字符串
  const html = renderToString(
    createElement(route.component, { data, params })
  );
  
  // 包装为完整文档
  const fullHtml = renderDocument(html, {
    head: renderHead(route, data),
    scripts: route.hasInteractivity ? ['/hydrate.js'] : [],
    styles: ['/styles.css'],
  });
  
  // 写入文件
  const filePath = path.join(outputDir, route.path, 'index.html');
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, fullHtml);
}

Implementing ISR (Incremental Static Regeneration)

实现ISR(增量静态再生)

javascript
// ISR IMPLEMENTATION

class ISRCache {
  constructor() {
    this.cache = new Map();
    this.regenerating = new Set();
  }
  
  async get(key, regenerate, { revalidate = 60 }) {
    const entry = this.cache.get(key);
    const now = Date.now();
    
    if (entry) {
      const age = (now - entry.timestamp) / 1000;
      
      // Fresh - return cached
      if (age < revalidate) {
        return { html: entry.html, status: 'HIT' };
      }
      
      // Stale - return cached but regenerate in background
      if (!this.regenerating.has(key)) {
        this.regenerating.add(key);
        this.regenerateInBackground(key, regenerate);
      }
      
      return { html: entry.html, status: 'STALE' };
    }
    
    // Miss - generate synchronously
    const html = await regenerate();
    this.cache.set(key, { html, timestamp: now });
    
    return { html, status: 'MISS' };
  }
  
  async regenerateInBackground(key, regenerate) {
    try {
      const html = await regenerate();
      this.cache.set(key, { html, timestamp: Date.now() });
    } finally {
      this.regenerating.delete(key);
    }
  }
  
  // On-demand revalidation
  revalidate(key) {
    this.cache.delete(key);
  }
  
  // Revalidate by tag
  revalidateTag(tag) {
    for (const [key, entry] of this.cache) {
      if (entry.tags?.includes(tag)) {
        this.cache.delete(key);
      }
    }
  }
}

// HTTP handler with ISR
const isrCache = new ISRCache();

async function handleISR(request) {
  const url = new URL(request.url);
  const route = matchRoute(url.pathname);
  
  const { html, status } = await isrCache.get(
    url.pathname,
    async () => {
      const data = await route.loader({ request });
      return renderToString(createElement(route.component, { data }));
    },
    { revalidate: route.revalidate || 60 }
  );
  
  return new Response(html, {
    headers: {
      'Content-Type': 'text/html',
      'X-Cache-Status': status,
      'Cache-Control': `s-maxage=${route.revalidate}, stale-while-revalidate`,
    },
  });
}
javascript
// ISR实现

class ISRCache {
  constructor() {
    this.cache = new Map();
    this.regenerating = new Set();
  }
  
  async get(key, regenerate, { revalidate = 60 }) {
    const entry = this.cache.get(key);
    const now = Date.now();
    
    if (entry) {
      const age = (now - entry.timestamp) / 1000;
      
      // 新鲜内容 - 返回缓存
      if (age < revalidate) {
        return { html: entry.html, status: 'HIT' };
      }
      
      // 旧内容 - 返回缓存并在后台再生
      if (!this.regenerating.has(key)) {
        this.regenerating.add(key);
        this.regenerateInBackground(key, regenerate);
      }
      
      return { html: entry.html, status: 'STALE' };
    }
    
    // 缓存未命中 - 同步生成
    const html = await regenerate();
    this.cache.set(key, { html, timestamp: now });
    
    return { html, status: 'MISS' };
  }
  
  async regenerateInBackground(key, regenerate) {
    try {
      const html = await regenerate();
      this.cache.set(key, { html, timestamp: Date.now() });
    } finally {
      this.regenerating.delete(key);
    }
  }
  
  // 按需再生
  revalidate(key) {
    this.cache.delete(key);
  }
  
  // 按标签再生
  revalidateTag(tag) {
    for (const [key, entry] of this.cache) {
      if (entry.tags?.includes(tag)) {
        this.cache.delete(key);
      }
    }
  }
}

// 带ISR的HTTP处理器
const isrCache = new ISRCache();

async function handleISR(request) {
  const url = new URL(request.url);
  const route = matchRoute(url.pathname);
  
  const { html, status } = await isrCache.get(
    url.pathname,
    async () => {
      const data = await route.loader({ request });
      return renderToString(createElement(route.component, { data }));
    },
    { revalidate: route.revalidate || 60 }
  );
  
  return new Response(html, {
    headers: {
      'Content-Type': 'text/html',
      'X-Cache-Status': status,
      'Cache-Control': `s-maxage=${route.revalidate}, stale-while-revalidate`,
    },
  });
}

Edge Rendering Architecture

边缘渲染架构

javascript
// EDGE RENDERING IMPLEMENTATION

// Edge-compatible rendering (V8 isolates)
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    // Check edge cache first
    const cacheKey = new Request(url.toString(), request);
    const cache = caches.default;
    let response = await cache.match(cacheKey);
    
    if (!response) {
      // Render at edge
      const html = await renderPage(url.pathname, {
        // Edge has access to geolocation
        country: request.cf?.country,
        city: request.cf?.city,
      });
      
      response = new Response(html, {
        headers: {
          'Content-Type': 'text/html',
          'Cache-Control': 's-maxage=60',
        },
      });
      
      // Cache at edge
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
    }
    
    return response;
  },
};

// Personalization at the edge
async function renderPage(pathname, context) {
  const route = matchRoute(pathname);
  
  // Personalize based on location
  const data = await route.loader({
    country: context.country,
    locale: countryToLocale(context.country),
  });
  
  return renderToString(createElement(route.component, { data }));
}
javascript
// 边缘渲染实现

// 边缘兼容渲染(V8隔离环境)
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    // 先检查边缘缓存
    const cacheKey = new Request(url.toString(), request);
    const cache = caches.default;
    let response = await cache.match(cacheKey);
    
    if (!response) {
      // 边缘渲染
      const html = await renderPage(url.pathname, {
        // 边缘可获取地理位置
        country: request.cf?.country,
        city: request.cf?.city,
      });
      
      response = new Response(html, {
        headers: {
          'Content-Type': 'text/html',
          'Cache-Control': 's-maxage=60',
        },
      });
      
      // 缓存到边缘
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
    }
    
    return response;
  },
};

// 边缘个性化
async function renderPage(pathname, context) {
  const route = matchRoute(pathname);
  
  // 根据位置个性化
  const data = await route.loader({
    country: context.country,
    locale: countryToLocale(context.country),
  });
  
  return renderToString(createElement(route.component, { data }));
}

Related Skills

相关技能

  • See web-app-architectures for SPA vs MPA
  • See seo-fundamentals for SEO implications
  • See hydration-patterns for hydration strategies
  • 查看 web-app-architectures 了解SPA与MPA
  • 查看 seo-fundamentals 了解SEO影响
  • 查看 hydration-patterns 了解水合策略