Rendering strategy thường bị biến thành lựa chọn framework: đánh dấu một tùy chọn lúc khởi tạo dự án rồi áp dụng nó cho mọi màn hình. Cách nhìn đó che mất quyết định thật. SSR, CSR, SSG và ISR mô tả HTML được tạo khi nào, dữ liệu được đọc ở đâu và kết quả được tái sử dụng trong bao lâu. Một sản phẩm có thể dùng cả bốn, thậm chí kết hợp nhiều cách trên cùng một route.

Câu hỏi hữu ích không phải là “acronym nào nhanh nhất?” Một tài liệu static trong CDN có thể có time to first byte rất tốt nhưng hiển thị tồn kho cũ. Trang tài khoản render ở server có thể gửi nội dung có nghĩa sớm nhưng tiêu tốn quá nhiều compute trên mỗi request. Dashboard render ở client cá nhân hóa tự nhiên, nhưng người dùng có thể phải nhìn shell rỗng trong lúc JavaScript và dữ liệu đi qua mạng. Mỗi chiến lược chuyển latency, độ mới, độ phức tạp và lỗi sang một phần khác của hệ thống.

Bài viết này xây dựng decision model theo từng route. Ta sẽ theo dõi byte từ data source đến HTML, rồi từ HTML đến giao diện tương tác được, sau đó nối các thời điểm ấy với SEO, Core Web Vitals, caching, personalization và chi phí vận hành.

Bắt đầu bằng hai timeline: HTML và dữ liệu

Browser chưa thể hiển thị nội dung ứng dụng cho đến khi nhận đủ HTML hoặc JavaScript để tạo nội dung đó. Nó cũng chưa thể xử lý tương tác đến khi code tương ứng được tải và event handler được gắn. Đây là các mốc riêng biệt:

  • HTML availability: lúc element có nghĩa xuất hiện trong response hoặc DOM.
  • Data availability: lúc đã biết các giá trị cần điền vào element.
  • Visual availability: lúc browser đã style, layout và paint nội dung.
  • Interactivity: lúc hành động của người dùng chạy được logic dự kiến.

Với CSR, response đầu thường chỉ có root element và script tag. Dữ liệu đến sau khi JavaScript chạy. Với SSR, server đọc dữ liệu và phát HTML có nghĩa trong request. Với SSG, việc đọc và render diễn ra lúc build. ISR bắt đầu bằng tài liệu đã generate nhưng cho phép tạo lại sau freshness window.

sequenceDiagram participant B as Browser participant E as CDN hoặc edge participant A as App server participant D as Data source B->>E: GET /products/42 alt Static hoặc ISR cache còn mới E-->>B: HTML trong cache else SSR hoặc regeneration E->>A: Chuyển tiếp request A->>D: Đọc dữ liệu của route D-->>A: Dữ liệu sản phẩm A-->>E: HTML và serialized state E-->>B: HTML response end B->>E: Yêu cầu JavaScript chunk E-->>B: JavaScript B->>B: Hydrate vùng tương tác

Hydration là cầu nối giữa HTML do server tạo và ứng dụng phía client. Framework dựng lại component tree, liên kết tree đó với DOM hiện có rồi cài behavior. Hydration không miễn phí: browser vẫn phải download, parse và execute code dù pixel đã xuất hiện. Vì vậy một trang có thể có Largest Contentful Paint (LCP) tốt nhưng Interaction to Next Paint (INP) kém nếu hydration tạo long task trên main thread.

Note

Server rendering không đồng nghĩa với bỏ client JavaScript. Nó thay đổi thứ user và crawler nhận được trước khi JavaScript chạy. Islands, partial hydration và server-only component có thể giảm lượng code cần hydrate, nhưng SSR thông thường vẫn gửi cả HTML lẫn application code.

CSR đặt rendering và cá nhân hóa trong browser

Client-side rendering phục vụ một shell dùng chung, sau đó browser fetch dữ liệu và dựng view. Cách này phù hợp với công cụ đã đăng nhập, nơi phần lớn nội dung phụ thuộc người dùng và không cần search index: editor, analytics console, inbox hoặc màn hình vận hành nội bộ.

Một Vue route nên hiển thị loading state trung thực và hủy request cũ khi input đổi:

vue
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue';
import { getProject, type Project } from './project-api.js';

const props = defineProps<{ projectId: string }>();
const project = ref<Project>();
const error = ref<string>();
let controller: AbortController | undefined;

watch(
  () => props.projectId,
  async (projectId) => {
    controller?.abort();
    const requestController = new AbortController();
    controller = requestController;
    project.value = undefined;
    error.value = undefined;

    try {
      project.value = await getProject(projectId, requestController.signal);
    } catch (cause) {
      if (!requestController.signal.aborted) {
        error.value = cause instanceof Error ? cause.message : 'Request failed';
      }
    }
  },
  { immediate: true },
);

onBeforeUnmount(() => controller?.abort());
</script>

<template>
  <p v-if="error" role="alert">{{ error }}</p>
  <ProjectSkeleton v-else-if="!project" />
  <ProjectWorkspace v-else :project="project" />
</template>

CSR giảm tải generate HTML và giúp cache shell ở CDN dễ dàng. Navigation sau lúc khởi động có thể rất nhanh vì runtime được giữ lại và chỉ fetch data. Tuy nhiên cold visit tạo một dependency chain: HTML, JavaScript, execute, API request rồi mới render. Bundle lớn, thiết bị chậm, API lỗi, script bị chặn hoặc runtime exception đều có thể khiến trang không còn nội dung hữu ích. Loading placeholder cần giữ kích thước ổn định để dữ liệu đến muộn không gây Cumulative Layout Shift (CLS).

Search engine có thể chạy JavaScript, nhưng phụ thuộc vào khả năng đó làm việc khám phá nội dung chậm và thiếu chắc chắn hơn. Link preview, crawler đơn giản, accessibility tool và user dùng thiết bị yếu đều hưởng lợi từ source HTML có nghĩa. Vì thế CSR không phải default tốt cho bài viết công khai hoặc trang sản phẩm mà nội dung chính là lý do người dùng truy cập. Nó vẫn rất phù hợp với bề mặt riêng tư, nhiều tương tác nếu có route-level code splitting, request cancellation, error state hữu ích và bundle budget được đo đạc.

SSR tính response cho từng request

Server-side rendering đọc context của request, load dữ liệu và tạo HTML trước khi trả response. Nó tự nhiên phù hợp với nội dung phải mới hoặc được cá nhân hóa ngay ở first paint: storefront theo locale với giá hiện tại, search result theo vị trí hoặc trang tài khoản có navigation phụ thuộc permission.

SSR tăng khả năng đạt LCP tốt vì browser có thể paint nội dung mà không đợi application code fetch data. Crawler cũng nhận đủ title, heading, link, structured data và canonical metadata. Chi phí xuất hiện ở TTFB và server capacity. Trước khi byte đầu tiên rời server, hệ thống có thể phải authenticate, gọi nhiều service và render component tree. Một dependency chậm trở thành response chậm của mọi visitor.

Streaming SSR nới lỏng rào cản này. Server gửi document shell và các vùng đã xong trong khi boundary chậm tiếp tục render. Streaming cải thiện cảm giác tiến triển và tránh đợi query chậm nhất, nhưng không làm query ấy nhanh hơn. Status code và header có thể đã commit trước khi lỗi muộn xảy ra; crawler và cache vẫn phải nhận được tài liệu cuối cùng nhất quán.

Hydration correctness là một boundary khác. Client phải render cấu trúc ban đầu giống server. Đọc window, đồng hồ hiện tại, random value hay browser storage trong initial render có thể gây mismatch. Hãy serialize chính xác request data theo cách an toàn rồi dùng nó làm initial state ở client:

ts
import { createSSRApp, type Component } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { escapeJsonForHtml } from './html.js';
import { loadProduct } from './products.js';

export async function renderProductPage(
  App: Component,
  productId: string,
): Promise<string> {
  const product = await loadProduct(productId);
  const state = { product };
  const app = createSSRApp(App, state);
  const body = await renderToString(app);

  return `<!doctype html>
<html lang="vi">
  <body>
    <div id="app">${body}</div>
    <script>window.__STATE__=${escapeJsonForHtml(state)}</script>
    <script type="module" src="/assets/client.js"></script>
  </body>
</html>`;
}

escapeJsonForHtml phải vô hiệu hóa ký tự có thể đóng script element; chèn thẳng kết quả JSON.stringify vào HTML chưa phải biện pháp XSS đầy đủ. Browser entry phải truyền cùng state vào createSSRApp trước khi mount. Không đưa secret hoặc field chỉ dành cho server vào serialized state.

Failure mode của SSR gồm dependency fan-out, database pool cạn, render exception, memory pressure và retry storm từ proxy phía trước. Hãy giới hạn request deadline, chạy song song các read độc lập, cache dữ liệu dùng chung, giới hạn concurrency và trả degraded page có chủ đích khi service không thiết yếu lỗi. Cần đo p95 và p99 render time thay vì chỉ nhìn average.

SSG và ISR chuyển công việc khỏi request path

Static site generation resolve dữ liệu và tạo HTML trong deployment build. File kết quả có thể được phục vụ từ object storage hoặc CDN mà không cần application compute khi cache hit. Với documentation, bài viết, campaign page và catalog ổn định, cách này đem lại TTFB thấp, resilience rộng, SEO dễ dự đoán và attack surface nhỏ.

flowchart LR C[Content hoặc database] --> B[Build process] T[Template và component] --> B B --> H[HTML và asset có version] H --> O[Object storage] O --> E[CDN cache] E --> U[Browser] W[Publish webhook] --> Q[Build hoặc revalidation queue] Q --> B

Đổi lại, độ mới phụ thuộc deployment. Một correction chưa xuất hiện cho đến khi build thành công và artifact mới tới serving layer. Build duration tăng theo số route và data fan-out. Full build một triệu trang sản phẩm có thể chậm và đắt hơn render on demand cho tập nhỏ đang có traffic. Build failure có thể chặn update không liên quan; route bị xóa cũng cần cleanup hoặc redirect rõ ràng.

Incremental Static Regeneration giữ static serving model nhưng refresh từng route. Implementation khác nhau, song stale-while-revalidate thường hoạt động như sau: cached page còn fresh trong một interval; sau khi hết hạn, một request kích hoạt regeneration trong khi visitor tiếp tục nhận tài liệu stale; render thành công sẽ thay entry theo cách atomic. Một số hệ thống block request đầu hoặc regenerate từ publication event.

Policy đó phải trả lời chính xác nhiều câu hỏi. Interval là maximum age hay chỉ là thời điểm sớm nhất được revalidate? Nếu regeneration lỗi thì sao? Mỗi region có cache riêng không? Khi giá hoặc navigation đổi, các page liên quan bị invalidate thế nào? Thiếu request coalescing, route phổ biến vừa hết hạn có thể tạo thundering herd. Thiếu versioning và atomic replacement, user có thể nhận output chưa hoàn chỉnh.

Warning

Không dùng shared HTML cache cho response chứa dữ liệu theo user hoặc được bảo vệ bằng permission, trừ khi cache key và Vary phân vùng đầy đủ mọi chiều liên quan. Default an toàn nhất là không cache private HTML và fetch fragment cá nhân hóa riêng.

So sánh hiệu năng, SEO, độ mới và chi phí

Không chiến lược nào sở hữu độc quyền một metric. Architecture thay đổi critical path; implementation quyết định kết quả. Trang SSG nặng vẫn có thể có LCP và INP tệ. Streaming SSR gọn có thể nhanh hơn trên route động. Hãy dùng field data được phân đoạn theo route, thiết bị, địa lý và cache status.

Chiến lược HTML có nghĩa đầu tiên Độ mới thường gặp Nền tảng SEO Cá nhân hóa Chi phí vận hành chính
CSR Sau khi chạy JS và fetch data API data tại request time Yếu nếu không prerender Tự nhiên trong browser API capacity và phân phối client bundle
SSR Trong từng request Request-time Mạnh Mạnh nhưng hạn chế shared cache Render compute, dependency, observability
SSG Trong build Deployment-time Mạnh Thường qua fragment phía client Build, storage, cache invalidation
ISR Build hoặc background regeneration Staleness có giới hạn hoặc theo event Mạnh Shared page cộng dynamic fragment Regeneration và phối hợp distributed cache

TTFB đo tốc độ response bắt đầu trả byte. SSG và ISR từ CDN thường rất tốt; SSR cộng thêm routing, data và render work; CSR có thể có shell TTFB nhanh nhưng con số đó không nói nội dung đã sẵn sàng. LCP đo lúc nội dung nhìn thấy lớn nhất được paint. HTML sớm có lợi, nhưng image priority, CSS, font, server latency và client rendering đều ảnh hưởng. INP phản ánh interaction latency trong toàn bộ visit. Hydration quá mức, long task và third-party script làm metric này xấu đi bất kể HTML xuất phát ở đâu.

SEO còn nhiều hơn body text đã render. Mỗi URL indexable cần status code ổn định, canonical URL, language alternate, metadata, structured data và link crawler đi theo được. Không trả 200 cho record SSR không tồn tại và không generate hàng nghìn tổ hợp parameter ít giá trị. SSG có thể phát hiện broken link trong build; SSR phải enforce invariant tương tự ở runtime.

Personalization và cacheability kéo theo hai hướng ngược nhau. Thay vì render toàn bộ homepage theo từng user, có thể cache editorial page dùng chung rồi load một recommendation region nhỏ sau authentication. Edge-side include hoặc composable CDN có thể ghép fragment nhưng bổ sung cache key và failure mode. Hybrid đơn giản nhất thường là public HTML cộng private island phía client.

Chọn theo từng route và vận hành hybrid có chủ đích

Hãy bắt đầu từ invariant của route, không phải framework default. Hỏi nội dung được phép cũ bao lâu, anonymous user có dùng chung nó không, nội dung first visit có cần index không, data nào cần xuất hiện above the fold và phần nào vẫn phải dùng được khi JavaScript hoặc origin service lỗi.

Route Default hợp lý Lý do
Bài documentation SSG Công khai, cache tốt, đổi khi publish
Bản tin SSG hoặc ISR theo event Nhiều lượt đọc, có publication event rõ ràng
Chi tiết sản phẩm ISR cộng fragment giá/tồn kho live Mô tả dùng chung, dữ liệu thương mại biến động
Kết quả tìm kiếm SSR hoặc CSR tùy SEO policy Phụ thuộc query và có thể tốn tài nguyên
Account settings SSR shell hoặc CSR Riêng tư, cá nhân hóa, không index
Collaborative editor CSR sau entry shell SSR/SSG Tương tác và live state chiếm ưu thế

Một sản phẩm hybrid có thể static generate category page, incrementally regenerate mô tả sản phẩm, server-render cart và client-render address selector của checkout. Ngay trong một route cũng có thể dùng static frame, availability summary từ SSR và purchase control được hydrate. Cần làm ownership rõ ràng: ghi lại cache key, freshness target, invalidation trigger, fallback và service chịu trách nhiệm cho từng vùng render.

Về vận hành, hãy quan sát cache hit ratio, regeneration duration và failure, origin render latency, build queue age, deployment time, hydration error, số byte JavaScript và Web Vitals theo route. Gắn cache status và dependency timing vào SSR trace. Test cold cache và slow origin chứ không chỉ warm navigation trên máy local. Giữ last known good static artifact khi build lỗi; nếu regeneration lỗi, phục vụ stale content trong giới hạn tường minh. Với security correction hoặc giá sai, phải có purge path bỏ qua freshness window thông thường.

Chi phí đi theo nơi công việc được thực hiện. SSG trả build compute và storage; SSR trả theo traffic; CSR chuyển rendering sang thiết bị user nhưng vẫn trả API và script delivery; ISR trả hạ tầng cache và độ phức tạp invalidation. Hãy so sánh cost trên mỗi visit thành công, bao gồm observability, incident và developer time. Request path rẻ nhất vẫn có thể tạo hệ thống đắt nhất để hiểu và duy trì.

Những điểm cần nhớ

  • Rendering strategy là quyết định về lịch thực thi và caching: HTML cùng data có lúc nào, công việc chạy ở đâu và output được tái sử dụng bao lâu.
  • CSR phù hợp ứng dụng riêng tư, nhiều tương tác, nhưng cold path phụ thuộc JavaScript execution rồi mới đến data request.
  • SSR cung cấp request-time HTML và personalization mạnh, đồng thời thêm server latency, hydration constraint và failure mode trên từng request.
  • SSG loại origin khỏi hot path; ISR bổ sung độ mới có kiểm soát nhưng cần regeneration lock, atomic replacement, invalidation và stale policy.
  • TTFB, LCP và INP mô tả ba giai đoạn khác nhau. Cần tối ưu và đo cả ba cùng cache status và route context.
  • SEO và resilience tốt hơn khi có HTML ý nghĩa không phụ thuộc JavaScript, nhưng metadata, status code, link và fallback vẫn phải được thiết kế rõ ràng.
  • Hãy chọn ở cấp route hoặc component. Một hybrid được mô tả minh bạch thường trung thực và hiệu quả hơn ép mọi page vào cùng một acronym.