ByteNoteByteNote

字节笔记本

2026年2月20日

SSR Stubs 技术详解:优化 Cloudflare Workers 部署的包体积

API中转
¥120

SSR stubs(服务端渲染存根)是一种优化技术,用于在构建时将仅客户端使用的重型库替换为空实现,从而显著减小 SSR bundle 的体积。本文深入解析其原理、实现方式及在 Cloudflare Workers 部署中的实际应用。

什么是 SSR Stubs

SSR stubs 是一种构建时优化技术,核心思想是:

在服务端渲染(SSR)过程中,某些库只在浏览器环境使用,服务端并不需要。通过将这些库替换为"空实现"(stub),可以大幅减小服务端 bundle 体积。

典型应用场景

场景客户端需求服务端需求优化效果
图表库(Mermaid)渲染流程图无需渲染减少 ~2 MiB
代码高亮(Shiki)语法高亮无需高亮减少 ~500 KiB
WASM 模块高性能计算不兼容 Workers避免运行时错误
富文本编辑器交互编辑仅展示 HTML减少 ~1 MiB

工作原理

构建流程

text
源代码
   ↓
Vite 构建
   ├── 客户端构建 → 完整库代码
   └── SSR 构建   → stubs 替换重型库
                        ↓
                   生成精简的 SSR bundle

实现机制

通过 Vite 插件在 SSR 构建时拦截模块导入:

typescript
// vite.config.ts
const ssrOnlyStubs = () => ({
  name: 'ssr-only-stubs',
  enforce: 'pre',
  resolveId(id, importer, { ssr }) {
    // 仅在 SSR 构建时生效
    if (!ssr) return null;
    
    // 匹配需要 stub 的库
    if (id === 'beautiful-mermaid') {
      return '\0stub:beautiful-mermaid';
    }
  },
  load(id) {
    if (id === '\0stub:beautiful-mermaid') {
      // 返回空实现
      return 'export default () => {}';
    }
  }
});

VibeAny 中的实现

项目中的 Stubs 配置

feat/cloudflare 分支的 vite.config.ts 中:

typescript
const ssrOnlyStubs = () => ({
  name: 'ssr-only-stubs',
  enforce: 'pre',
  resolveId(id, importer, { ssr }) {
    if (!ssr) return null;
    
    // Mermaid 相关库 - 客户端渲染图表
    if (id.includes('beautiful-mermaid') || 
        id.includes('@streamdown/')) {
      return '\0stub:noop';
    }
    
    // Shiki 语言定义 - 客户端代码高亮
    if (id.includes('shiki/langs/')) {
      return '\0stub:empty-array';
    }
    
    // WASM 引擎 - Workers 不兼容
    if (id.includes('@shikijs/engine-oniguruma')) {
      return '\0stub:js-engine';
    }
  },
  load(id) {
    switch (id) {
      case '\0stub:noop':
        return 'export default () => {};';
      case '\0stub:empty-array':
        return 'export default [];';
      case '\0stub:js-engine':
        return 'export { createJavaScriptRegexEngine } from "@shikijs/engine-javascript";';
    }
  }
});

包体积优化效果

优化项原始大小优化后节省
Mermaid + Cytoscape~2.0 MiB0~2.0 MiB
Shiki 语言定义~500 KiB0~500 KiB
WASM 运行时~200 KiBJS 引擎 ~50 KiB~150 KiB
总计~2.7 MiB~50 KiB~2.65 MiB

与 Cloudflare Workers 的结合

Workers 的限制

Cloudflare Workers 免费版有以下限制:

  • Bundle 大小: gzip 后不超过 3 MiB
  • 运行时: 不支持 Node.js 原生模块
  • WASM: 需要特殊配置,部分功能受限

为什么 SSR Stubs 对 Workers 至关重要

未优化前的 bundle 分析:

text
原始 SSR bundle:
├── React/Vue 框架代码     ~300 KiB
├── 业务逻辑代码           ~200 KiB
├── Mermaid 图表库        ~1200 KiB  ❌ 服务端不需要
├── Cytoscape 图论库       ~800 KiB  ❌ 服务端不需要
├── Shiki 语法定义         ~500 KiB  ❌ 服务端不需要
├── WASM 运行时            ~200 KiB  ❌ Workers 不兼容
└── 其他依赖              ~300 KiB

总计: ~3.5 MiB (超过 3 MiB 限制 ❌)

使用 SSR stubs 优化后:

text
优化后 SSR bundle:
├── React/Vue 框架代码     ~300 KiB
├── 业务逻辑代码           ~200 KiB
├── Mermaid stub            ~0 KiB   ✅
├── Cytoscape stub          ~0 KiB   ✅
├── Shiki 精简版           ~50 KiB   ✅
├── JS 正则引擎            ~50 KiB   ✅
└── 其他依赖              ~300 KiB

总计: ~900 KiB (远低于 3 MiB 限制 ✅)

实现自定义 Stub

步骤 1: 识别重型客户端库

分析 bundle 找出大体积的仅客户端库:

bash
# 使用 wrangler 分析 bundle 大小
wrangler deploy --dry-run --outdir .wrangler/dist

# 查看各文件大小
ls -lh .wrangler/dist/

步骤 2: 创建 Stub 模块

typescript
// stubs/mermaid.ts
export default {
  render: () => Promise.resolve(''),
  initialize: () => {},
};

// stubs/cytoscape.ts  
export default () => ({
  add: () => {},
  layout: () => ({ run: () => {} }),
});

步骤 3: 配置 Vite 插件

typescript
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    {
      name: 'custom-ssr-stubs',
      enforce: 'pre',
      resolveId(id, importer, { ssr }) {
        if (!ssr) return null;
        
        // 映射到本地 stub 文件
        const stubs: Record<string, string> = {
          'mermaid': '/stubs/mermaid.ts',
          'cytoscape': '/stubs/cytoscape.ts',
        };
        
        if (stubs[id]) {
          return stubs[id];
        }
      }
    }
  ]
});

最佳实践

1. 渐进式采用

不要一次性 stub 所有库,按优先级逐步优化:

  1. 高优先级: 体积 > 500 KiB 的纯客户端库
  2. 中优先级: 体积 100-500 KiB 的非关键库
  3. 低优先级: 体积 < 100 KiB 的库(收益有限)

2. 保持客户端功能完整

SSR stubs 只影响服务端渲染,客户端行为保持不变:

typescript
// 组件代码无需修改
import Mermaid from 'beautiful-mermaid';

function Diagram({ code }) {
  useEffect(() => {
    // 客户端正常执行
    Mermaid.render(code);
  }, []);
  
  // 服务端返回占位符
  return <div className="mermaid-placeholder">{code}</div>;
}

3. 动态导入进一步优化

对于非首屏必需的库,使用动态导入:

typescript
// 代替静态导入
// import HeavyChart from 'heavy-chart-lib';

// 使用动态导入
const HeavyChart = lazy(() => import('heavy-chart-lib'));

function ChartPage() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <HeavyChart data={data} />
    </Suspense>
  );
}

常见问题

"Module not found" 错误

确保 stub 导出的 API 与实际库兼容:

typescript
// 错误:缺少必要导出
export default {};

// 正确:模拟库的 API 结构
export default {
  render: () => Promise.resolve(''),
  parse: () => ({}),
  initialize: () => {},
};

类型定义丢失

为 stub 创建类型声明文件:

typescript
// stubs/mermaid.d.ts
declare module 'beautiful-mermaid' {
  export function render(code: string): Promise<string>;
  export function initialize(config: any): void;
}

客户端 hydrate 不匹配

确保服务端 stub 返回的 HTML 结构与客户端一致:

typescript
// 服务端 stub
export function renderToString() {
  // 返回与客户端一致的占位结构
  return '<div class="chart-container"></div>';
}

总结

SSR stubs 是解决 Cloudflare Workers 3 MiB 限制的有效方案:

优势说明
零运行时开销构建时完成替换,不影响运行时性能
透明化组件代码无需修改,客户端行为不变
可扩展易于添加新的 stub 规则
类型安全可配合 TypeScript 类型定义

通过合理使用 SSR stubs,VibeAny 项目成功将 SSR bundle 从 3.5 MiB 优化到 900 KiB,同时保持完整的客户端功能。

相关文章

分享: