JavaScript: Trình Duyệt vs Server — Thực Sự Khác Nhau Chỗ Nào?

JavaScript là cùng một ngôn ngữ dù chạy trên Chrome hay trên server. Cùng const, cùng for loop, cùng Promise. Vậy tại sao JavaScript phía server lại khác biệt đến vậy?
Bởi vì ngôn ngữ chỉ là một nửa câu chuyện. Nửa còn lại là môi trường chạy — và môi trường trình duyệt với server hoàn toàn khác nhau.
Bài viết này sẽ mổ xẻ chính xác điều gì thay đổi khi JavaScript rời khỏi trình duyệt và chạy trên server với Node.js, Bun, hay Deno.
Bạn Sẽ Học Được Gì
Kết Quả Học Tập:
✅ Tại sao cùng một ngôn ngữ lại hoạt động khác nhau ở hai môi trường
✅ Browser cho bạn những API nào vs server cho bạn những gì
✅ Sự khác biệt giữa global object, module, và async pattern
✅ Điểm khác nhau chính giữa Node.js, Bun, và Deno
✅ Những lỗi thường gặp khi chuyển đổi giữa code browser và server
✅ Cách viết code chạy được ở cả hai môi trường
Ý Tưởng Cốt Lõi: Cùng Động Cơ, Khác Bảng Điều Khiển
Hãy tưởng tượng JavaScript như động cơ ô tô. Động cơ (V8, SpiderMonkey, JavaScriptCore) chạy cùng một JavaScript code. Nhưng bảng điều khiển — các nút bấm, công tắc, đồng hồ đo — phụ thuộc vào xe nào đang gắn động cơ đó.
Bản đặc tả JavaScript (ECMAScript) chỉ định nghĩa ngôn ngữ: biến, hàm, class, promise, iterator. Mọi thứ còn lại — thao tác DOM, đọc ghi file, HTTP server — đều đến từ môi trường chạy.
Phần 1: Trình Duyệt Cho Bạn Những Gì
Khi JavaScript chạy trong trình duyệt (Chrome, Firefox, Safari), bạn được truy cập Web API do nhà sản xuất trình duyệt cung cấp. Các API này được chuẩn hóa bởi W3C và WHATWG.
Global Object window
Trong trình duyệt, global object cấp cao nhất là window. Mọi thứ đều treo trên nó:
// Browser globals
console.log(window.location.href); // URL hiện tại
console.log(window.innerWidth); // Chiều rộng viewport
console.log(window.navigator.userAgent); // Thông tin trình duyệt
// "window" được ngầm hiểu
alert("Hello!"); // Giống window.alert("Hello!")
setTimeout(() => {}, 1000); // Giống window.setTimeout(...)DOM — Vũ Khí Bí Mật Của Trình Duyệt
Document Object Model là API cho phép JavaScript tương tác với HTML. Nó không tồn tại trên server.
// Thao tác DOM — CHỈ trên trình duyệt
const button = document.getElementById("submit-btn");
button.addEventListener("click", () => {
const input = document.querySelector(".email-input");
const div = document.createElement("div");
div.textContent = `Đã gửi: ${input.value}`;
document.body.appendChild(div);
});Tại sao chỉ có trên trình duyệt: DOM là biểu diễn trực tiếp của trang HTML đang render trên màn hình. Server không có màn hình, không có trang HTML, không có user đang click nút.
Các API Chỉ Có Trên Trình Duyệt
Đây là những API bạn dùng được trên trình duyệt nhưng không dùng được trên server (trừ khi polyfill):
| API | Chức Năng |
|---|---|
document / DOM | Thao tác các HTML element |
window.location | URL hiện tại và điều hướng |
window.history | Lịch sử trình duyệt (back/forward) |
localStorage / sessionStorage | Lưu trữ key-value trên trình duyệt |
navigator.geolocation | Vị trí GPS của user |
Canvas / WebGL | Render đồ họa 2D/3D |
Web Audio API | Xử lý âm thanh |
IntersectionObserver | Theo dõi element có hiển thị không |
Service Workers | Cache offline và đồng bộ nền |
WebRTC | Video/audio peer-to-peer |
Cách JavaScript Trình Duyệt Được Load
Browser JS được load qua thẻ <script> và chạy trong môi trường sandbox:
<!-- Load script truyền thống -->
<script src="app.js"></script>
<!-- ES Module trong trình duyệt -->
<script type="module" src="app.mjs"></script>
<!-- Inline script -->
<script>
console.log("Tôi chạy trên trình duyệt!");
</script>Ràng buộc quan trọng: JavaScript trên trình duyệt bị sandbox. Nó không thể:
- Đọc hoặc ghi file trên máy tính của user
- Truy cập tab trình duyệt khác (same-origin policy)
- Gửi request đến domain khác mà không có CORS
- Chạy lệnh hệ thống
Sandbox tồn tại vì bảo mật — bạn không muốn một website random đọc file trên máy bạn đâu.
Phần 2: Server Cho Bạn Những Gì
Khi JavaScript chạy trên server runtime (Node.js, Bun, hoặc Deno), sandbox biến mất. Bạn được truy cập trực tiếp hệ điều hành.
Global Object — Không Phải window Nữa
Mỗi runtime có global riêng:
// Node.js
console.log(globalThis); // Cách chuẩn (hoạt động mọi nơi)
console.log(global); // Riêng Node.js (legacy)
// Deno
console.log(globalThis); // Dùng globalThis chuẩn
// Deno.version, Deno.env, etc.
// Bun
console.log(globalThis); // Dùng globalThis chuẩn
// Bun.version, Bun.env, etc.Mẹo: Dùng
globalThis— nó hoạt động trên browser, Node.js, Deno, và Bun. Đây là cách chuẩn để tham chiếu global object.
Truy Cập File System
Khác biệt lớn nhất: server có thể đọc và ghi file.
// Node.js
import { readFile, writeFile } from "node:fs/promises";
const data = await readFile("config.json", "utf-8");
const config = JSON.parse(data);
config.updatedAt = new Date().toISOString();
await writeFile("config.json", JSON.stringify(config, null, 2));
// Deno
const data = await Deno.readTextFile("config.json");
await Deno.writeTextFile("config.json", newContent);
// Bun
const file = Bun.file("config.json");
const data = await file.text();
await Bun.write("config.json", newContent);Tạo HTTP Server
Trên trình duyệt, bạn gửi HTTP request. Trên server, bạn nhận chúng.
// Node.js (built-in)
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Hello from Node.js!" }));
});
server.listen(3000);
// Deno
Deno.serve({ port: 3000 }, (req) => {
return new Response(JSON.stringify({ message: "Hello from Deno!" }), {
headers: { "Content-Type": "application/json" },
});
});
// Bun
Bun.serve({
port: 3000,
fetch(req) {
return new Response(JSON.stringify({ message: "Hello from Bun!" }), {
headers: { "Content-Type": "application/json" },
});
},
});Các API Chỉ Có Trên Server
| API | Chức Năng |
|---|---|
fs (File System) | Đọc, ghi, xóa file và thư mục |
path | Xử lý đường dẫn file đa nền tảng |
http / https | Tạo HTTP server và client |
net | Network TCP/UDP cấp thấp |
child_process | Chạy chương trình và script khác |
os | Thông tin CPU, RAM, hostname |
crypto | Hashing, mã hóa, chứng chỉ |
stream | Xử lý dữ liệu lớn theo chunk |
process / Deno.env / Bun.env | Biến môi trường và thông tin process |
worker_threads | Song song hóa đa luồng |
Phần 3: So Sánh Song Song
Đặt hai môi trường cạnh nhau cho dễ thấy:
| Khía Cạnh | Trình Duyệt | Server (Node.js / Bun / Deno) |
|---|---|---|
| Global object | window | globalThis / global / Deno |
| Truy cập DOM | Có | Không (trừ khi dùng JSDOM) |
| File system | Không | Có (fs, Deno.readFile, Bun.file) |
| Vai trò HTTP | Client (gửi request) | Server (nhận request) |
| Module system | ES Modules qua <script type="module"> | ES Modules + CommonJS (Node.js) |
| Package manager | Không có (dùng CDN hoặc bundler) | npm, yarn, pnpm, bun |
| Bảo mật | Sandbox (không truy cập OS) | Toàn quyền OS (hoặc permission-based với Deno) |
| Tương tác user | Click, bàn phím, touch event | stdin, CLI argument, HTTP request |
| Concurrency | Web Workers | Worker Threads, Cluster, Child Processes |
| Entry point | Thẻ <script> trong HTML | node app.js / bun app.ts / deno run app.ts |
Phần 4: Module System — Phần Hay Rối Nhất
Một trong những nguồn gây bối rối lớn nhất khi chuyển giữa browser và server JavaScript chính là module.
Trình Duyệt: Chỉ Có ES Modules
Trình duyệt hiện đại hỗ trợ ES Modules gốc:
<script type="module">
import { formatDate } from "./utils.js";
console.log(formatDate(new Date()));
</script>Quy tắc trên trình duyệt:
- Phải có đuôi file (
.js,.mjs) - Import dùng URL tương đối (
./utils.js, không phảiutils) - Không bare import kiểu
import express from "express"(trừ khi dùng import maps) - Load bất đồng bộ, mặc định defer
Server: Phức Tạp Hơn Một Chút
Node.js lớn lên với CommonJS và sau đó mới thêm ES Modules. Bun và Deno mặc định dùng ES Modules.
// CommonJS (Node.js legacy — vẫn dùng rất nhiều)
const express = require("express");
const path = require("path");
module.exports = { myFunction };
// ES Modules (Hiện đại — chạy trên Node, Bun, Deno)
import express from "express";
import { join } from "node:path";
export function myFunction() {}Cách mỗi runtime xử lý module:
| Tính Năng | Node.js | Bun | Deno |
|---|---|---|---|
| Mặc định | CommonJS | ES Modules | ES Modules |
| ES Modules | .mjs hoặc "type": "module" trong package.json | Luôn luôn | Luôn luôn |
| CommonJS | .cjs hoặc mặc định | Hỗ trợ | Hỗ trợ hạn chế |
| Import từ URL | Không | Không | Có (import x from "https://...") |
| Bare imports | Có (import x from "pkg") | Có | Có (với import map hoặc prefix npm:) |
// Deno — có thể import từ URL
import { serve } from "https://deno.land/std/http/server.ts";
// Deno — npm package với prefix
import express from "npm:express";
// Node.js — built-in module giờ dùng prefix node:
import { readFile } from "node:fs/promises";Phần 5: Các API Tồn Tại Ở Cả Hai Nơi
Qua nhiều năm, trình duyệt và server runtime đã hội tụ ở nhiều API. Giờ đây chúng hoạt động ở cả hai môi trường:
Fetch API
Ban đầu chỉ có trên trình duyệt, giờ dùng được mọi nơi:
// Chạy được trên browser, Node.js 18+, Bun, và Deno
const response = await fetch("https://api.example.com/users");
const users = await response.json();
console.log(users);Web Crypto API
// Chạy được mọi nơi
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode("hello")
);Các API Dùng Chung Khác
| API | Tình Trạng |
|---|---|
fetch() | Có ở tất cả môi trường hiện đại |
URL / URLSearchParams | Có ở mọi nơi |
TextEncoder / TextDecoder | Có ở mọi nơi |
crypto.subtle | Có ở mọi nơi |
AbortController | Có ở mọi nơi |
structuredClone() | Có ở mọi nơi |
setTimeout / setInterval | Có ở mọi nơi |
console.* | Có ở mọi nơi |
WebSocket | Có ở mọi nơi |
ReadableStream / WritableStream | Có ở mọi nơi |
Blob / File | Có ở mọi nơi (Node.js 18+) |
FormData | Có ở mọi nơi (Node.js 18+) |
Sự hội tụ này là tin mừng — nghĩa là ngày càng nhiều code của bạn portable giữa các môi trường.
Phần 6: Ba Server Runtime So Sánh Nhanh
Giờ hãy so sánh nhanh điểm khác biệt giữa Node.js, Bun, và Deno khi làm server.
Để đọc chi tiết hơn, xem Bun vs Node.js vs Deno: Hướng Dẫn So Sánh Toàn Diện.
Node.js — Ông Trùm Lâu Năm
// Phong cách Node.js
import express from "express";
import { readFile } from "node:fs/promises";
const app = express();
app.get("/config", async (req, res) => {
const data = await readFile("./config.json", "utf-8");
res.json(JSON.parse(data));
});
app.listen(3000);Thế mạnh: Hệ sinh thái khổng lồ, đã qua thử lửa, hỗ trợ phổ biến nhất, nhu cầu tuyển dụng cao nhất.
Deno — Lựa Chọn An Toàn
// Phong cách Deno — TypeScript sẵn có
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router();
router.get("/config", async (ctx) => {
const data = await Deno.readTextFile("./config.json");
ctx.response.body = JSON.parse(data);
});
const app = new Application();
app.use(router.routes());
await app.listen({ port: 3000 });Thế mạnh: TypeScript sẵn có, bảo mật theo quyền hạn, API theo chuẩn web, import từ URL.
Bun — Tốc Độ Là Số 1
// Phong cách Bun — tối giản
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/config") {
const file = Bun.file("./config.json");
return new Response(file, {
headers: { "Content-Type": "application/json" },
});
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`Listening on ${server.url}`);Thế mạnh: Startup và runtime nhanh nhất, bundler/test runner tích hợp sẵn, thay thế Node.js gần như hoàn toàn.
So Sánh Nhanh Các Runtime
| Khía Cạnh | Node.js | Deno | Bun |
|---|---|---|---|
| TypeScript | Cần tsx hoặc ts-node | Tích hợp sẵn | Tích hợp sẵn |
| Bảo mật | Toàn quyền OS mặc định | Theo quyền hạn (--allow-read, etc.) | Toàn quyền OS mặc định |
| Hiệu năng | Tốt | Tốt | Nhanh nhất |
| Tương thích npm | Gốc | Cao (qua prefix npm:) | Cao |
| Độ ổn định | Trưởng thành nhất | Ổn định | Ổn định |
| Phù hợp cho | Enterprise, dự án có sẵn | App cần bảo mật cao | App cần hiệu năng cao |
Phần 7: Lỗi Hay Gặp Khi Chuyển Đổi
1. window is Not Defined
Lỗi số 1 khi chạy code browser trên server:
// ❌ Crash trên server
if (window.innerWidth > 768) {
showMobileMenu();
}
// ✅ Kiểm tra trước
if (typeof window !== "undefined") {
// Đang ở trình duyệt
if (window.innerWidth > 768) {
showMobileMenu();
}
}2. document is Not Defined
Cùng vấn đề — DOM không tồn tại trên server:
// ❌ Crash trên server
const el = document.getElementById("app");
// ✅ Guard lại
if (typeof document !== "undefined") {
const el = document.getElementById("app");
}3. __dirname và __filename Trong ES Modules
CommonJS có sẵn __dirname và __filename, nhưng ES Modules thì không:
// CommonJS — chạy được
console.log(__dirname); // /home/user/project
console.log(__filename); // /home/user/project/app.js
// ES Modules — phải tự tính
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Bun và Deno có shortcut
console.log(import.meta.dir); // Bun
console.log(import.meta.dirname); // Node.js 21+4. Fetch Không Cần Import Nữa
Trước Node.js 18, phải cài node-fetch. Giờ fetch có sẵn mọi nơi:
// ❌ Cách cũ (Node.js < 18)
import fetch from "node-fetch";
// ✅ Hiện đại — cứ xài thôi
const res = await fetch("https://api.example.com/data");5. require() vs import
Trộn lẫn module system sẽ gây lỗi:
// ❌ Không dùng được require() trong ES Module
import express from "express";
const config = require("./config"); // SyntaxError!
// ✅ Dùng import nhất quán
import express from "express";
import config from "./config.js";
// ✅ Hoặc dùng dynamic import
const config = await import("./config.js");Phần 8: Viết Code Chạy Được Mọi Nơi (Isomorphic)
Đôi khi bạn cần code chạy được cả trên browser và server. Đây là các pattern thực tế:
Pattern 1: Phát Hiện Môi Trường
const isBrowser = typeof window !== "undefined";
const isServer = typeof window === "undefined";
const isNode = typeof process !== "undefined" && process.versions?.node;
const isDeno = typeof Deno !== "undefined";
const isBun = typeof Bun !== "undefined";
if (isBrowser) {
// Code riêng cho browser
localStorage.setItem("theme", "dark");
} else {
// Code riêng cho server
console.log("Đang chạy trên server");
}Pattern 2: Conditional Exports Trong package.json
{
"name": "my-library",
"exports": {
".": {
"browser": "./dist/browser.js",
"node": "./dist/server.js",
"default": "./dist/server.js"
}
}
}Pattern 3: Trừu Tượng Hóa Sự Khác Biệt
// storage.js — chạy được mọi nơi
export function getStorage() {
if (typeof window !== "undefined") {
// Browser: dùng localStorage
return {
get: (key) => localStorage.getItem(key),
set: (key, value) => localStorage.setItem(key, value),
};
} else {
// Server: dùng in-memory store (hoặc file/database)
const store = new Map();
return {
get: (key) => store.get(key),
set: (key, value) => store.set(key, value),
};
}
}Phần 9: Tổng Quan Bằng Hình
Phần 10: Khi Nào Cần Quan Tâm?
Đây là hướng dẫn thực tế:
Bạn đang viết code chỉ cho browser nếu:
- Bạn thao tác HTML/CSS (DOM)
- Bạn xử lý event của user (click, bàn phím)
- Bạn dùng
localStorage,sessionStorage, hoặc cookie quadocument.cookie - Bạn đang build frontend với React, Vue, Angular, v.v.
Bạn đang viết code chỉ cho server nếu:
- Bạn đọc/ghi file
- Bạn kết nối database
- Bạn tạo HTTP endpoint
- Bạn truy cập biến môi trường hoặc tài nguyên hệ thống
- Bạn chạy background job hoặc cron task
Bạn đang viết code universal nếu:
- Bạn build thư viện dùng ở cả hai môi trường
- Bạn làm việc với framework như Next.js (SSR + client)
- Bạn viết utility function (format ngày, validation, xử lý string)
Tóm Tắt Và Bài Học Chính
-
Cùng ngôn ngữ, khác môi trường. Cú pháp JavaScript giống hệt ở mọi nơi. Các API thay đổi tùy theo nơi chạy.
-
Trình duyệt = client bị sandbox. Bạn có DOM, Web API, và gửi request. Không có file system, không truy cập OS.
-
Server = runtime không giới hạn. Bạn có file I/O, HTTP server, truy cập OS, và các thao tác cấp hệ thống.
-
Các API đang hội tụ.
fetch,crypto,URL,streams— nhiều API giờ chạy được ở cả hai môi trường. -
Module system vẫn còn khác nhau. CommonJS là di sản server, ES Modules là tương lai. Deno và Bun mặc định dùng ESM.
-
Dùng
globalThisvà kiểm tra môi trường để code portable. -
Ba runtime (Node.js, Deno, Bun) cho bạn cùng sức mạnh với các đánh đổi khác nhau về bảo mật, hiệu năng, và trải nghiệm dev.
Đọc Thêm
- Bun vs Node.js vs Deno: Hướng Dẫn So Sánh Toàn Diện — chi tiết về sự khác biệt giữa các runtime
- Bắt Đầu Với Express.js — build server đầu tiên
- HTTP Protocol: Hướng Dẫn Toàn Diện — hiểu giao thức đằng sau web request
- JavaScript Build Tools & Bundlers Giải Thích — code đi từ source đến browser thế nào
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.