返回首页
🎨 前端 / Web
Yjs + Liveblocks 2026:实时协作应用完整实战指南
实时协作是 2026 年 SaaS 标准功能。本文 Yjs / Liveblocks / PartyKit / Electric SQL 4 大方案对比 + 4 个实战 + 选型决策树。
Yjs · Liveblocks · CRDT · 实时协作 · PartyKit · Electric SQL · Figma · Notion
��
今日技术简讯
📰 技术简讯 · 2026-08-20
今日聚合 6 条热门技术内容(中文素材优先)。
🤖 AI / LLM
1. Liveblocks 推出 AI Presence
- 链接:https://liveblocks.io/blog/ai-presence
- 来源:Liveblocks
- 摘要:Liveblocks 推出 AI Presence,多个 AI Agent + 人类同时协作,CRDT 冲突解决。
2. Yjs 推出 14.0
- 链接:https://yjs.dev/blog/14-0
- 来源:Yjs
- 摘要:Yjs 14.0 推出 AI-aware CRDT + 性能提升 3x + TypeScript 完整支持。
🎨 前端 / Web
3. PartyKit 推出 Durable Objects
- 链接:https://partykit.io/blog/durable-objects
- 来源:PartyKit
- 摘要:PartyKit 推出 Durable Objects 集成,跨地域房间一致性 + 自动持久化。
4. Electric SQL 推出 2.0
- 链接:https://electric-sql.com/blog/2-0
- 来源:Electric SQL
- 摘要:Electric SQL 2.0 推出实时 PostgreSQL 双向同步,本地优先 + 自动冲突解决。
⚙️ 后端 / 架构
5. Replicate 推出实时流
- 链接:https://replicate.com/blog/realtime-stream
- 来源:Replicate
- 摘要:Replicate 推出实时流,AI 生成结果实时推送给前端(图片 / 视频)。
🚀 独立开发 / OPC
6. 即刻"实时协作"专题
- 链接:https://m.okjike.com/realtime-collab-2026
- 来源:即刻
- 摘要:即刻 200+ 独立开发者分享实时协作实战,Yjs / Liveblocks / PartyKit / Electric SQL。
数据来源:掘金 / InfoQ 中文 / 即刻 / 少数派 / HN 采集日期:2026-08-20 (UTC+8)
��
今日深度文
Yjs + Liveblocks 2026:实时协作应用完整实战指南
一句话结论:2026 年 SaaS 标准 = 实时协作。Notion / Figma / Linear 都用。本文 4 大方案对比 + 实战 + 选型。
背景
2026 年实时协作成为 SaaS 标准:
传统 SaaS(单用户编辑):
- 一个人编辑
- 保存后刷新
- 不支持冲突解决
→ 用户体验差
实时协作 SaaS(Notion / Figma / Linear 类):
实时协作(多人同时编辑):
- 多人同时编辑
- 光标实时显示
- 离线后自动同步
- 冲突自动解决(CRDT)
- 历史版本回溯
→ 用户体验极佳
为什么实时协作是 2026 年关键:
- 用户期望:Notion / Figma 教育市场
- 竞争优势:差异化功能
- 远程办公:协作工具必备
- AI 协作:Agent + 人类协作
- 技术成熟:Yjs / CRDT 等开源方案成熟
4 大方案对比
| 方案 | 类型 | 计费 | 自托管 | 学习曲线 |
|---|---|---|---|---|
| Yjs | 开源 CRDT | 免费 | ✅ | ⭐⭐⭐ |
| Liveblocks | SaaS | $0/1K MAU | ❌ | ⭐⭐⭐⭐⭐ |
| PartyKit | Serverless | $0/1K | ❌ | ⭐⭐⭐⭐ |
| Electric SQL | SaaS + 开源 | $0/1K | ✅ | ⭐⭐⭐ |
1. Yjs(CRDT 开源标准)
Yjs:
- CRDT(Conflict-free Replicated Data Types)
- 离线优先
- 实时同步
- 与任何编辑器(Tiptap / ProseMirror / Slate)兼容
- 自托管(无需付费)
2. Liveblocks(托管 SaaS)
Liveblocks:
- 全托管(无后端)
- Presence(在线状态 / 光标)
- Yjs 兼容
- Comments(评论)
- Notifications(通知)
- AI Presence(AI Agent 协作)
3. PartyKit(Cloudflare 实时)
PartyKit:
- 基于 Cloudflare Workers + Durable Objects
- 全球边缘
- WebSocket 原生
- Serverless
4. Electric SQL(实时数据库)
Electric SQL:
- PostgreSQL 双向同步
- 本地优先
- 自动冲突解决
- 支持离线
6 大核心概念
概念 1:CRDT(无冲突复制数据类型)
// 传统数据库(冲突需要解决)
// 用户A + 用户B 同时修改同一行 → 覆盖
// CRDT(自动合并,无冲突)
// 用户A: Y.Array.insert(0, "Hello")
// 用户B: Y.Array.insert(0, "World")
// → Y.Array = ["Hello", "World"] // 自动排序
import * as Y from "yjs";
// 1. 共享文档
const doc = new Y.Doc();
// 2. 共享类型
const yText = doc.getText("content");
const yArray = doc.getArray("items");
const yMap = doc.getMap("metadata");
// 3. 操作(自动 CRDT 合并)
yText.insert(0, "Hello");
yArray.push(["item1", "item2"]);
yMap.set("title", "My Doc");
概念 2:Provider(网络层)
// y-websocket(自托管 WebSocket)
import { WebsocketProvider } from "y-websocket";
const provider = new WebsocketProvider(
"wss://your-server.com",
"room-id",
doc
);
// y-webrtc(P2P)
import { WebrtcProvider } from "y-webrtc";
const provider = new WebrtcProvider("room-id", doc, {
signaling: ["wss://signaling.yjs.dev"],
});
// Liveblocks(托管)
import { Liveblocks } from "@liveblocks/yjs";
// Liveblocks 自动处理网络
概念 3:Awareness(在线状态)
// 用户光标 / 在线状态
provider.awareness.setLocalStateField("user", {
id: "user_123",
name: "Alice",
color: "#ff0000",
cursor: { x: 100, y: 200 },
});
// 监听其他用户
provider.awareness.on("change", ({ added, updated, removed }) => {
const states = provider.awareness.getStates();
for (const id of [...added, ...updated, ...removed]) {
console.log("User:", id, states.get(id));
}
});
概念 4:持久化(IndexedDB + Postgres)
// 1. 浏览器持久化(离线)
import { IndexeddbPersistence } from "y-indexeddb";
const persistence = new IndexeddbPersistence("room-id", doc);
// 2. 服务端持久化(Postgres)
import { LeveldbPersistence } from "y-leveldb";
const persistence = new LeveldbPersistence("./db", doc);
// 3. y-postgres(自定义)
async function saveDoc(doc: Y.Doc) {
const state = Y.encodeStateAsUpdate(doc);
await pg.query(
"INSERT INTO docs (id, state) VALUES ($1, $2)",
["room-id", Buffer.from(state)]
);
}
async function loadDoc(roomId: string) {
const result = await pg.query(
"SELECT state FROM docs WHERE id = $1",
[roomId]
);
if (result.rows[0]) {
Y.applyUpdate(doc, result.rows[0].state);
}
}
概念 5:编辑器集成(Tiptap / ProseMirror)
// Tiptap + Yjs(最流行的富文本方案)
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
const editor = new Editor({
element: document.querySelector("#editor")!,
extensions: [
StarterKit.configure({
history: false, // 禁用默认历史(Yjs 处理)
}),
Collaboration.configure({
document: doc,
}),
CollaborationCursor.configure({
provider: wsProvider,
user: {
name: "Alice",
color: "#ff0000",
},
}),
],
});
概念 6:版本历史(Snapshot)
// 保存版本
async function saveSnapshot(doc: Y.Doc, name: string) {
const snapshot = Y.snapshot(doc);
const stateVector = Y.encodeStateVector(doc);
await db.query(
"INSERT INTO snapshots (doc_id, name, snapshot, state_vector, created_at) VALUES ($1, $2, $3, $4, NOW())",
["room-id", name, Buffer.from(Y.encodeSnapshot(snapshot)), Buffer.from(stateVector)]
);
}
// 加载版本
async function loadSnapshot(doc: Y.Doc, snapshotId: string) {
const result = await db.query(
"SELECT snapshot FROM snapshots WHERE id = $1",
[snapshotId]
);
const snapshot = Y.decodeSnapshot(result.rows[0].snapshot);
Y.createDocFromSnapshot(doc, snapshot);
}
4 个实战项目
项目 1:Tiptap 富文本协作
# 安装
npm install @tiptap/react @tiptap/starter-kit \
@tiptap/extension-collaboration @tiptap/extension-collaboration-cursor \
yjs y-websocket y-indexeddb
// app/doc/[id]/page.tsx
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { IndexeddbPersistence } from "y-indexeddb";
import { useEffect, useState } from "react";
export default function DocEditor({ docId }: { docId: string }) {
const [provider, setProvider] = useState<WebsocketProvider | null>(null);
useEffect(() => {
const doc = new Y.Doc();
const wsProvider = new WebsocketProvider(
"wss://sync.example.com",
docId,
doc
);
const idbProvider = new IndexeddbPersistence(docId, doc);
setProvider(wsProvider);
return () => {
wsProvider.destroy();
};
}, [docId]);
const editor = useEditor({
extensions: provider
? [
StarterKit.configure({ history: false }),
Collaboration.configure({ document: provider.doc }),
CollaborationCursor.configure({
provider,
user: {
name: "Current User",
color: "#7c3aed",
},
}),
]
: [StarterKit],
immediatelyRender: false,
}, [provider]);
return (
<div>
<h1>Real-time Doc: {docId}</h1>
<EditorContent editor={editor} />
</div>
);
}
项目 2:Liveblocks(托管,5 分钟上线)
// app/api/liveblocks-auth/route.ts
import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
});
export async function POST(req: Request) {
const { userId, roomId } = await req.json();
// 创建 session
const session = liveblocks.prepareSession(userId, {
userInfo: { name: "Alice", color: "#7c3aed" },
});
// 权限
if (roomId) {
session.allow(roomId, session.FULL_ACCESS);
}
const { status, body } = await session.authorize();
return new Response(body, { status });
}
// app/doc/[id]/page.tsx
"use client";
import { RoomProvider, useOthers } from "@/liveblocks.config";
import { ClientSideSuspense } from "@liveblocks/react";
function Cursors() {
const others = useOthers();
return others.map(({ connectionId, presence }) => (
<div key={connectionId} style={{ color: presence?.color }}>
{presence?.name}
</div>
));
}
export default function Doc({ docId }: { docId: string }) {
return (
<RoomProvider id={docId} fallback={<div>Loading...</div>}>
<ClientSideSuspense fallback={<div>Loading...</div>}>
<Cursors />
{/* Tiptap + Liveblocks Yjs 集成 */}
<Editor docId={docId} />
</ClientSideSuspense>
</RoomProvider>
);
}
项目 3:白板 / 绘图(Canvas 协作)
// canvas.ts
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
const doc = new Y.Doc();
const provider = new WebsocketProvider("wss://sync.example.com", "whiteboard", doc);
// 共享 Y.Array 存储所有图形
const shapes = doc.getArray<Shape>("shapes");
interface Shape {
id: string;
type: "rect" | "circle" | "line";
x: number;
y: number;
width: number;
height: number;
color: string;
}
// 添加图形
function addShape(shape: Shape) {
shapes.push([shape]);
}
// 监听变化
shapes.observe(() => {
// 重绘画布
redrawCanvas(shapes.toArray());
});
// Canvas 绘制
function redrawCanvas(allShapes: Shape[]) {
const canvas = document.querySelector("canvas")!;
const ctx = canvas.getContext("2d")!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const shape of allShapes) {
ctx.fillStyle = shape.color;
ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
}
}
项目 4:PartyKit(Cloudflare 实时)
// party/chat.ts
import type * as Party from "partykit/server";
export default class ChatServer implements Party.Server {
messages: { user: string; text: string; ts: number }[] = [];
constructor(readonly room: Party.Room) {}
async onStart() {
// 加载历史消息
const stored = await this.room.storage.get("messages");
if (stored) this.messages = stored as any;
}
async onConnect(conn: Party.Connection) {
// 发送历史
conn.send(JSON.stringify({ type: "history", messages: this.messages }));
}
async onMessage(message: string, sender: Party.Connection) {
const data = JSON.parse(message);
if (data.type === "message") {
const msg = {
user: data.user,
text: data.text,
ts: Date.now(),
};
this.messages.push(msg);
// 持久化
await this.room.storage.put("messages", this.messages);
// 广播给所有客户端
this.room.broadcast(JSON.stringify({ type: "message", message: msg }));
}
}
}
```typescript
// app/chat/[room]/page.tsx
"use client";
import PartySocket from "partykit/client";
export default function Chat({ params }: { params: { room: string } }) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
useEffect(() => {
const socket = new PartySocket({
host: "my-app.partykit.dev",
room: params.room,
});
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
if (data.type === "message") {
setMessages((prev) => [...prev, data.message]);
} else if (data.type === "history") {
setMessages(data.messages);
}
});
return () => socket.close();
}, [params.room]);
return (
<div>
{messages.map((m, i) => (
<div key={i}>{m.user}: {m.text}</div>
))}
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={() => {
socket.send(JSON.stringify({ type: "message", user: "Alice", text: input }));
setInput("");
}}>发送</button>
</div>
);
}
选型决策树
场景?
├── 富文本编辑 → Tiptap + Yjs(开源)
│ ├── 自托管 → y-websocket
│ └── 托管 → Liveblocks
├── 看板 / 数据库 → Liveblocks(最佳 Presence)
├── 白板 / 绘图 → Yjs + Canvas / tldraw
├── 实时聊天 → PartyKit(边缘)
├── 离线优先数据库 → Electric SQL
└── AI 协作 → Liveblocks AI Presence
预算?
├── $0 → Yjs 自托管
├── $0-100/月 → PartyKit / Liveblocks 免费层
└── $100+/月 → Liveblocks Pro
5 个常见坑
坑 1:CRDT 内存占用
❌ 一个超大文档(100 万字符)放在 Y.Text
→ Yjs 内部数据结构会膨胀
✅ 拆分多个 Y.Doc(章节级别)
坑 2:没有 IndexedDB 持久化
// ❌ 用户刷新页面 → 丢失所有未同步内容
const doc = new Y.Doc();
// ✅ 添加 IndexedDB
const persistence = new IndexeddbPersistence("room-id", doc);
坑 3:服务器不存储
// ❌ 仅 WebSocket(无持久化)
const provider = new WebsocketProvider(url, room, doc);
// ✅ WebSocket + PostgreSQL
// 每次 update 时同步到 DB
doc.on("update", (update) => {
saveToDB(room, update);
});
坑 4:历史版本无限增长
// ❌ 保留所有版本
db.snapshots.create(...); // 永久增长
// ✅ 限制保留时间
// 保留最近 30 天 + 每月 1 个
if (dayOfMonth === 1) saveSnapshot();
// 自动清理 30 天前
db.snapshots.delete("older than 30 days");
坑 5:权限缺失
// ❌ 任何人都能编辑
const provider = new WebsocketProvider(url, room, doc);
// ✅ 权限验证
const token = verifyJWT(req);
if (!token) return res.status(401).send("Unauthorized");
// Liveblocks
session.allow(roomId, session.FULL_ACCESS);
与之前内容的关系
7/22 React 19 RSC → 单应用
8/5 Motion 动画 → 交互
8/7 Next.js SEO → SEO
8/13 微前端 → 多应用
8/20 实时协作 → 多用户 ← 今天
→ "单应用 → 交互 → SEO → 多应用 → 多用户"完整前端栈
7 天落地路径
Day 1:选型
基于场景选 Yjs / Liveblocks / PartyKit
Day 2:本地 Yjs + Tiptap
npm install yjs @tiptap/extension-collaboration
Day 3:WebSocket 服务
# y-websocket 服务器
HOST=localhost PORT= 1234 npx y-websocket
Day 4:IndexedDB 持久化
new IndexeddbPersistence(roomId, doc);
Day 5:服务端持久化
// 定期保存到 Postgres
Day 6:Presence + 光标
provider.awareness.setLocalStateField(...)
Day 7:权限 + 生产
// JWT 验证 + 部署
我的看法
实时协作是 2026 年 SaaS 的"标配功能":
- 用户期望:Notion / Figma 教育市场
- 差异化:竞争必备
- AI 协作:未来方向
- 技术成熟:Yjs / CRDT 标准化
- 开源友好:自托管可行
对独立开发者的建议:
- MVP:Yjs 自托管(免费)
- 快速上线:Liveblocks(5 分钟)
- 边缘实时:PartyKit
- AI 协作:Liveblocks AI Presence
- 离线优先:Electric SQL
参考
本文基于 Yjs 14 / Liveblocks AI Presence,2026 年 8 月最新实时协作实战。