Hướng dẫn toàn tập Vite SSG cho Vue 3: Từ SPA đến Siêu Tốc Độ
Chào mừng các bạn đến với hướng dẫn chi tiết về Vite SSG. Nếu bạn đang phát triển ứng dụng Vue 3 và muốn trang web của mình nhanh như chớp, thân thiện tối đa với SEO và mang lại trải nghiệm người dùng đỉnh cao, thì bạn đã đến đúng nơi.
Trong bài viết này, chúng ta sẽ đi sâu vào vite-ssg
(v2), một công cụ tuyệt vời giúp biến ứng dụng Single Page Application (SPA) của bạn thành một trang web tĩnh (Static Site Generation - SSG) hoặc kết hợp cả SSG và SSR một cách mượt mà.
1. Tại sao phải là SSG? Động lực đằng sau tốc độ
Bạn đã bao giờ tự hỏi làm thế nào các trang tin tức, blog lớn hay các trang tài liệu lại có thể tải gần như ngay lập tức? Câu trả lời thường nằm ở Static Site Generation (SSG).
Khác với SPA truyền thống (phải tải JavaScript về rồi mới render nội dung), SSG thực hiện quá trình render này ngay tại thời điểm build.
Lợi ích kỹ thuật:
- Tốc độ thờ:
- TTFB (Time to First Byte) gần như bằng 0. Server chỉ cần trả về một file HTML tĩnh đã có sẵn, không cần xử lý gì thêm.
- FCP (First Contentful Paint) cực nhanh. Người dùng thấy nội dung ngay lập tức, không phải nhìn vào màn hình trắng chờ đợi.
- Trải nghiệm người dùng (UX) vượt trội: Tốc độ nhanh đồng nghĩa với sự hài lòng. Người dùng không phải chờ đợi, giúp tăng tương tác và giữ chân họ ở lại lâu hơn.
- SEO “thần sầu”: Googlebot và các công cụ tìm kiếm khác “yêu” HTML tĩnh. Chúng có thể dễ dàng đọc, hiểu và index toàn bộ nội dung trang web của bạn, giúp cải thiện thứ hạng tìm kiếm một cách tự nhiên.
Lợi ích kinh doanh & marketing:
- Tăng Organic Traffic: Thứ hạng cao hơn trên Google đồng nghĩa với việc có nhiều người dùng truy cập vào trang web của bạn hơn.
- Giảm Bounce Rate: Khi trang web tải nhanh, người dùng ít có khả năng thoát ra ngay lập tức.
- Tăng Conversion Rate: Trải nghiệm mượt mà và nội dung dễ tiếp cận khuyến khích người dùng thực hiện các hành động mong muốn (mua hàng, đăng ký, liên hệ…).
2. Tóm tắt tinh hoa: Flow hoạt động của Vite SSG
Để không bị “ngợp” bởi các khái niệm, hãy nắm vững 3 bước cốt lõi của Vite SSG:
- Prerender (Lúc build):
vite-ssg
khởi chạy ứng dụng Vue của bạn trong môi trường Node.js. Nó sẽ “ghé thăm” từng route (đường dẫn) mà bạn đã định nghĩa, thực thi code (ví dụ: fetch data từ API), và render ra các file.html
tĩnh tương ứng. - Serve HTML tĩnh: Khi người dùng truy cập, web server chỉ đơn giản là gửi file HTML đã được tạo sẵn này. Quá trình này cực kỳ nhanh.
- Hydration (Phía client): Sau khi trình duyệt tải xong file HTML và JavaScript, Vue.js sẽ “tiếp quản”. Quá trình này được gọi là Hydration. Vue sẽ “thổi hồn” vào trang HTML tĩnh, gắn các event listener, khôi phục state và biến nó trở lại thành một SPA đầy đủ chức năng mà không cần render lại từ đầu.
Lưu ý quan trọng về môi trường:
import.meta.env.SSR
: Một biến toàn cục do Vite cung cấp. Nó sẽ làtrue
khi code đang chạy trong môi trường Node.js (lúc build) vàfalse
khi chạy trên trình duyệt của người dùng.isClient
: Một biến tiện ích đượcvite-ssg
cung cấp trong callbackcreateApp
, giúp bạn dễ dàng kiểm tra môi trường hơn.
3. Thiết lập dự án từ con số 0
Chúng ta sẽ bắt đầu với một dự án Vue 3 mới tinh và thêm các “gia vị” cần thiết.
Cài đặt
Mở terminal và chạy các lệnh sau:
# 1. Tạo dự án Vue 3 mới (chọn "Yes" cho Vue Router)
npm create vue@latest my-ssg-app
# 2. Di chuyển vào thư mục dự án
cd my-ssg-app
# 3. Cài đặt các thư viện cần thiết
# vite-ssg: "Trái tim" của SSG
# @vueuse/head: Quản lý thẻ <head> cho SEO
# firebase: Để fetch dữ liệu từ Firestore (ví dụ)
npm install vite-ssg @vueuse/head firebase
Cấu hình package.json
Mở file package.json
và cập nhật phần scripts
:
// package.json
"scripts": {
"dev": "vite",
"build": "vite-ssg build",
"serve": "vite preview"
},
npm run dev
: Chạy server phát triển như bình thường.npm run build
: Đây là lệnh quan trọng nhất. Nó sẽ gọivite-ssg
để thực hiện quá trình prerender và tạo ra các file tĩnh trong thư mụcdist
.npm run serve
: Dùng để xem trước trang web sau khi đãbuild
thành công.
Giải thích các tùy chọn CLI của vite-ssg
Lệnh vite-ssg build
có một vài tùy chọn hữu ích:
--script <mode>
: Quyết định cách thẻ<script>
được chèn vào HTML. Mặc định làasync
. Bạn có thể đổi thànhdefer
.--format <format>
: Định dạng module cho bundle phía server. Mặc định làesm
. Có thể đổi thànhcjs
nếu cần.--mock
: Mock các API của trình duyệt (nhưlocalStorage
) khi chạy ở môi trường Node.js.--parallel
: Cho phép build các trang song song để tăng tốc độ build với các dự án lớn.
4. Cấu hình Router & main.js
Đây là bước cấu hình để vite-ssg
biết nó cần phải làm gì.
src/router/index.js
File này gần như không thay đổi so với một dự án Vue Router bình thường. Điều quan trọng là bạn phải export
mảng routes
.
// src/router/index.js
import { createRouter, createWebHistory } from "vue-router";
import Home from "../pages/Home.vue";
import Posts from "../pages/Posts.vue";
import PostDetail from "../pages/PostDetail.vue";
// Export mảng routes để vite-ssg có thể sử dụng
export const routes = [
{
path: "/",
name: "home",
component: Home,
},
{
path: "/posts",
name: "posts",
component: Posts,
},
{
// Route động, chúng ta sẽ xử lý nó sau
path: "/posts/:id",
name: "post-detail",
component: PostDetail,
},
];
// Không cần export router instance
const router = createRouter({
history: createWebHistory(),
routes,
});
src/main.js
- Trái tim của ứng dụng
Đây là nơi phép màu xảy ra. Thay vì createApp().mount('#app')
, chúng ta sẽ dùng ViteSSG
.
// src/main.js
import { ViteSSG } from "vite-ssg";
import { createHead } from "@vueuse/head";
import App from "./App.vue";
import { routes } from "./router"; // Import mảng routes
// Import CSS global (nếu có)
import "./assets/main.css";
// Hàm `createApp` sẽ được vite-ssg gọi ở cả server và client
export const createApp = ViteSSG(
// 1. Component App gốc
App,
// 2. Tùy chọn, quan trọng nhất là `routes`
{
routes,
base: import.meta.env.BASE_URL,
// Các tùy chọn khác:
// script: 'async', // 'async' hoặc 'defer'
// format: 'esm', // 'esm' hoặc 'cjs'
},
// 3. Callback để cấu hình thêm cho app
({ app, router, routes, isClient, initialState }) => {
// Cài đặt plugin @vueuse/head
const head = createHead();
app.use(head);
// Bạn có thể cài đặt các plugin khác ở đây
// Ví dụ: app.use(Pinia)
// initialState chứa dữ liệu được fetch từ server (sẽ nói rõ ở phần sau)
if (isClient) {
// Ở phía client, ta có thể khôi phục state từ initialState
// Ví dụ: pinia.state.value = initialState.pinia || {}
}
}
);
Giải thích các tham số của ViteSSG
:
App
: ComponentApp.vue
gốc của bạn.options
:routes
: Bắt buộc.vite-ssg
cần biết các đường dẫn để prerender.base
: Đường dẫn gốc của ứng dụng, thường lấy từimport.meta.env.BASE_URL
.includedRoutes(paths, routes)
: Một hàm cho phép bạn thêm các route động vào danh sách prerender. Ví dụ, bạn có thể fetch danh sách ID bài viết và tạo ra các route/posts/1
,/posts/2
,…excludedRoutes
: Loại bỏ các route không cần prerender.
callback
: Một hàm được gọi sau khi app được tạo. Đây là nơi lý tưởng để cài đặt các plugin (Vuex, Pinia, VueUse Head…). Các tham số của nó rất hữu ích:app
: instance của Vue app.router
: instance của Vue Router.isClient
:true
nếu đang ở trình duyệt,false
nếu đang ở server (lúc build).initialState
: Một object rỗng ở server, nhưng ở client nó sẽ chứa dữ liệu được truyền từ server xuống.
5. Flow Build & Hydrate chi tiết
Hãy mổ xẻ quá trình này một cách chi tiết hơn.
Giai đoạn Build (trên server):
- Bạn chạy lệnh
npm run build
. vite-ssg
khởi tạo app Vue của bạn trong môi trường Node.js.- Nó duyệt qua tất cả các route tĩnh (
/
,/posts
). - Đối với các route động (như
/posts/:id
), nó sẽ gọi hàmincludedRoutes
(nếu bạn định nghĩa trongvite.config.js
) để lấy danh sách các đường dẫn cụ thể cần render (ví dụ:['/posts/post-1', '/posts/post-2']
). - Với mỗi route: a. Hook
onBeforeRender(route, initialState)
được gọi (nếu có trong component của route đó). Đây là nơi bạn fetch dữ liệu cho trang. Dữ liệu được gán vàoinitialState
. b. Component của route được render thành chuỗi HTML. c. Dữ liệu tronginitialState
được serialize và nhúng vào file HTML dưới dạng JSON trong thẻ<script>
. d. HookonPageRendered(route, renderedHTML)
được gọi. Bạn có thể chỉnh sửa HTML ở đây nếu muốn. e. File HTML cuối cùng được lưu vào thư mụcdist
.
Giai đoạn Client (trên trình duyệt):
- Người dùng truy cập URL, server trả về file HTML tĩnh tương ứng. Trình duyệt hiển thị ngay lập tức.
- JavaScript được tải về.
- Hook
onClientEntry()
được gọi (nếu có trongmain.js
). ViteSSG
đọcinitialState
từ file HTML.- Quá trình Hydration bắt đầu: Vue “khớp” virtual DOM của nó với DOM tĩnh đã có, gắn các event listener mà không render lại.
initialState
được sử dụng để khôi phục trạng thái cho các component (ví dụ: quaprops
). - Hook
onRenderComplete()
được gọi sau khi hydration hoàn tất. Ứng dụng của bạn giờ đây là một SPA đầy đủ chức năng.
6. Prerender Data với Firebase
Đây là phần hấp dẫn nhất: làm sao để trang SSG có nội dung động? Chúng ta sẽ dùng hook onBeforeRender
và Firestore REST API.
Chuẩn bị
- Tạo một project trên Firebase và tạo một database Firestore.
- Tạo file
.env.local
ở thư mục gốc dự án:
Lưu ý: Dùng REST API với API Key cho Web là an toàn cho việc đọc dữ liệu public. Đừng bao giờ đưa Admin SDK credentials vào code frontend.VITE_FIREBASE_PROJECT_ID="your-project-id" VITE_FIREBASE_API_KEY="your-web-api-key"
Ví dụ: pages/Posts.vue
& pages/PostDetail.vue
pages/PostDetail.vue
- Fetch dữ liệu cho một trang cụ thể
Đây là component hiển thị chi tiết một bài viết.
<template>
<div v-if="post">
<h1>{{ post.title }}</h1>
<p>Được viết bởi: {{ post.author }}</p>
<div class="content" v-html="post.content"></div>
</div>
<div v-else-if="isLoading">
<p>Đang tải bài viết...</p>
</div>
<div v-else>
<p>Không tìm thấy bài viết.</p>
</div>
</template>
<script>
// Hook onBeforeRender được export riêng
// Nó sẽ được vite-ssg gọi lúc build time
export async function onBeforeRender(route, initialState) {
const postId = route.params.id;
const projectId = import.meta.env.VITE_FIREBASE_PROJECT_ID;
const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/posts/${postId}`;
try {
const res = await fetch(url);
if (!res.ok) throw new Error("Failed to fetch");
const data = await res.json();
// Biến đổi dữ liệu từ Firestore về dạng dễ dùng hơn
const postData = {
title: data.fields.title.stringValue,
content: data.fields.content.stringValue,
author: data.fields.author.stringValue,
};
// Gán dữ liệu vào initialState để truyền xuống client
initialState.post = postData;
} catch (error) {
console.error(`Error fetching post ${postId}:`, error);
// Bạn có thể xử lý lỗi ở đây, ví dụ redirect sang trang 404
}
}
</script>
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { useHead } from "@vueuse/head";
// 1. Nhận initialData thông qua props
const props = defineProps({
initialState: {
type: Object,
default: () => ({}),
},
});
const route = useRoute();
const post = ref(props.initialState?.post);
const isLoading = ref(false);
// 2. Fallback fetching ở client-side
// Cần thiết khi người dùng điều hướng từ trang khác đến trang này (SPA navigation)
onMounted(async () => {
if (!post.value) {
isLoading.value = true;
// Tái sử dụng logic fetch ở client
// (Trong thực tế, bạn nên tách logic fetch ra một hàm riêng)
const postId = route.params.id;
const projectId = import.meta.env.VITE_FIREBASE_PROJECT_ID;
const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/posts/${postId}`;
try {
const res = await fetch(url);
const data = await res.json();
post.value = {
title: data.fields.title.stringValue,
content: data.fields.content.stringValue,
author: data.fields.author.stringValue,
};
} catch (e) {
console.error(e);
} finally {
isLoading.value = false;
}
}
});
// 3. Quản lý SEO cho trang
useHead(() => ({
title: post.value?.title || "Bài viết",
meta: [{ name: "description", content: `Đọc bài viết ${post.value?.title}` }],
}));
</script>
Luồng hoạt động của ví dụ trên:
- Lúc build:
vite-ssg
chạyonBeforeRender
, fetch dữ liệu từ Firestore, và gán vàoinitialState.post
. Dữ liệu này được nhúng vào file HTML. - Lúc tải trang: Vue nhận
initialState
quaprops
,post.value
có dữ liệu ngay lập tức và hiển thị ra màn hình.onMounted
được gọi, nhưng vìpost.value
đã có, nó không làm gì cả. - Lúc điều hướng SPA: Khi người dùng click một link từ trang Home đến trang này, component được mount.
initialState
lúc này là rỗng.onMounted
sẽ được kích hoạt để fetch dữ liệu từ phía client.
7. Các Hooks & API quan trọng của vite-ssg
onBeforeRender(route, initialState)
: (Trong component) Chạy chỉ ở server trước khi render. Nơi hoàn hảo để fetch data.onPageRendered(route, html)
: (Trongvite.config.js
) Chạy chỉ ở server sau khi render. Dùng để chỉnh sửa HTML thô.useHead(options)
: (Trong component, từ@vueuse/head
) Cách tốt nhất để quản lý thẻ<head>
(title, meta, script…) một cách linh hoạt.onClientEntry(callback)
: (Trongmain.js
) Chạy chỉ ở client trước khi app được mount.onRenderComplete(callback)
: (Trongmain.js
) Chạy chỉ ở client sau khi hydration hoàn tất.import.meta.env.SSR
: (Bất cứ đâu) Biến boolean để kiểm tra code đang chạy ở server hay client.
8. Quản lý <head>
& SEO nâng cao với JSON-LD
Quản lý thẻ <head>
là tối quan trọng cho SEO.
@vueuse/head
là lựa chọn hàng đầu
So với các phương pháp cũ, @vueuse/head
cực kỳ mạnh mẽ vì nó là một composable, cho phép bạn:
- Sử dụng bên trong
<script setup>
. - Cập nhật thẻ
<head>
một cách linh hoạt dựa trên state của component. - Tự động xử lý cả ở server (lúc build) và client (lúc hydrate).
Thêm dữ liệu có cấu trúc (JSON-LD)
JSON-LD là một cách để cung cấp thông tin chi tiết về trang của bạn cho các công cụ tìm kiếm, giúp hiển thị kết quả tìm kiếm phong phú (rich snippets).
Ví dụ thêm JSON-LD cho một bài viết:
<script setup>
import { computed } from "vue";
import { useHead } from "@vueuse/head";
// ... (logic fetch post)
const post = ref({
/* Dữ liệu bài viết */
});
// Sử dụng computed để tạo JSON-LD một cách linh hoạt
const jsonLd = computed(() => ({
type: "application/ld+json",
children: JSON.stringify({
"@context": "[https://schema.org](https://schema.org)",
"@type": "Article",
headline: post.value.title,
author: {
"@type": "Person",
name: post.value.author,
},
datePublished: post.value.publishDate, // ví dụ: '2025-07-15T09:00:00+07:00'
// ... các trường khác
}),
}));
useHead(() => ({
title: post.value.title,
meta: [
/* ... */
],
script: [
// Chèn JSON-LD vào head
jsonLd.value,
],
}));
</script>
9. Build & Deploy
Sau khi đã hoàn tất, đây là lúc đưa sản phẩm ra thế giới.
Build và kiểm tra
# Build tất cả các trang thành file tĩnh trong thư mục /dist
npm run build
# Chạy một server local để xem trước kết quả từ thư mục /dist
npm run serve
Hãy mở trình duyệt và truy cập vào địa chỉ mà vite preview
cung cấp. “View Page Source” để thấy nội dung HTML đã được render đầy đủ.
Deploy lên Cloudflare Pages
Cloudflare Pages là lựa chọn hoàn hảo cho các trang SSG.
- Đẩy code của bạn lên một repo GitHub/GitLab.
- Trong dashboard Cloudflare, chọn “Workers & Pages” -> “Create application” -> “Pages”.
- Kết nối với repo của bạn.
- Trong phần “Build settings”:
- Framework preset: Chọn
Vite
. - Build command:
npm run build
- Build output directory:
dist
- Framework preset: Chọn
- Nhấn “Save and Deploy”. Xong!
Deploy lên Firebase Hosting
Firebase Hosting cũng là một lựa chọn tuyệt vời.
- Cài đặt Firebase CLI:
npm install -g firebase-tools
. - Đăng nhập:
firebase login
. - Khởi tạo hosting:
firebase init hosting
.- Chọn project Firebase của bạn.
- What do you want to use as your public directory? Nhập
dist
. - Configure as a single-page app (rewrite all urls to /index.html)? Chọn No. Đây là điểm khác biệt quan trọng. Vì chúng ta đã có các file HTML cho từng route, chúng ta không cần rewrite tất cả về
index.html
.
- Build dự án:
npm run build
. - Deploy:
firebase deploy
.
10. Tổng kết & Checklist hoàn chỉnh
Chúng ta đã đi qua một hành trình dài. SSG với Vite và Vue 3 không hề phức tạp nếu bạn nắm vững các khái niệm cốt lõi. Nó mang lại sức mạnh to lớn về hiệu năng và SEO mà các ứng dụng SPA truyền thống khó có thể sánh được.
Hãy dùng checklist này để đảm bảo bạn không bỏ sót bước nào khi triển khai dự án của mình.
- [ ] Khởi tạo dự án Vue 3 và chọn “Yes” cho Vue Router.
- [ ] Cài đặt các thư viện cần thiết:
vite-ssg
,@vueuse/head
. - [ ] Cập nhật
scripts
trongpackage.json
vớibuild: "vite-ssg build"
. - [ ] Chuyển đổi
src/main.js
để sử dụngViteSSG()
thay vìcreateApp()
. - [ ] Export mảng
routes
từ file cấu hình router. - [ ] Đối với các trang cần dữ liệu, tạo và export hàm
async function onBeforeRender()
. - [ ] Trong
onBeforeRender
, fetch dữ liệu và gán vàoinitialState
. - [ ] Trong
<script setup>
, nhậninitialState
quaprops
. - [ ] Thêm logic fetching fallback trong
onMounted
cho các lần điều hướng phía client. - [ ] Sử dụng
useHead()
trong mỗi component trang để quản lýtitle
,meta
và các thẻ<head>
khác. - [ ] Thêm JSON-LD để tăng cường SEO cho các trang quan trọng (bài viết, sản phẩm…).
- [ ] Sử dụng
import.meta.env.SSR
hoặcisClient
khi cần viết code chỉ chạy ở một môi trường cụ thể. - [ ] Chạy
npm run build
để tạo trang tĩnh. - [ ] Chạy
npm run serve
để kiểm tra kỹ lưỡng trang web thành phẩm. - [ ] Deploy lên một dịch vụ hosting tĩnh như Cloudflare Pages hoặc Firebase Hosting.
- [ ] Sử dụng Google PageSpeed Insights để đo lường và ăn mừng điểm số Core Web Vitals cao ngất ngưởng!
Chúc mừng bạn đã hoàn thành hướng dẫn! Giờ đây bạn đã có đủ kiến thức để xây dựng những ứng dụng Vue siêu tốc và chinh phục các công cụ tìm kiếm. Happy coding!