renderToString là đồng bộ và một cục: server phải dựng xong toàn bộ HTML mới gửi byte đầu tiên. Một query chậm ở cuối trang cũng giữ lại cả trang, và nó không hỗ trợ Suspense cho data fetching.
renderToPipeableStream (Node) — bản Web Streams là renderToReadableStream — stream HTML theo từng phần. Vùng nào chưa có dữ liệu thì gửi trước fallback của Suspense; khi dữ liệu về, server đẩy tiếp một khối HTML kèm script nhỏ để thay chỗ fallback, không cần client fetch lại.
const { pipe, abort } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
pipe(res) // start streaming as soon as the shell is ready
},
onShellError() {
res.statusCode = 500
res.send('<h1>Something went wrong</h1>')
},
onError(err) { logger.error(err) },
})
setTimeout(abort, 10_000) // stop waiting on slow dataonShellReady kích hoạt khi phần ngoài mọi Suspense boundary đã render xong — đây là mốc để bắt đầu stream, cho TTFB thấp. onAllReady đợi toàn bộ nội dung, kể cả trong Suspense. Dùng onAllReady cho crawler hoặc khi cần sinh HTML tĩnh; dùng onShellReady cho người dùng thật.
Lưu ý: chỉ đặt được status code và header trước khi bắt đầu pipe. Lỗi xảy ra sau đó đã ở giữa dòng stream nên phải xử lý ở phía client (onError chỉ để log).