Bỏ qua điều hướng

Node.js là gì? Môi trường chạy JavaScript ngoài trình duyệt

Node.js là runtime JavaScript dựa trên V8 engine và libuv, dùng kiến trúc event loop và I/O không chặn để xử lý hàng nghìn kết nối trên một luồng.

Tuan Tran Van
8 phút đọc
Mục lục (7 phần)
  1. Node.js là gì?
  2. Node.js khác gì JavaScript chạy trong trình duyệt?
  3. Event loop và I/O không chặn hoạt động ra sao?
  4. npm và hệ sinh thái gói của Node.js
  5. Khi nào nên dùng Node.js — và khi nào không?
  6. Nên bắt đầu với phiên bản nào?
  7. Tài liệu tham khảo

Node.js là một môi trường thực thi (runtime) JavaScript mã nguồn mở, đa nền tảng, cho phép chạy JavaScript bên ngoài trình duyệt — cụ thể là phía máy chủ hoặc trực tiếp trên hệ điều hành.

Ryan Dahl giới thiệu Node.js lần đầu vào năm 2009 cho hệ điều hành Linux, và từ năm 2012 nó bắt đầu được hỗ trợ chính thức trên Windows.

Sự xuất hiện của Node.js giúp bạn dùng một ngôn ngữ duy nhất cho cả frontendbackend. Việc này làm giảm đáng kể hiện tượng "context shift" (thay đổi ngữ cảnh) khi lập trình, và cho phép bạn chia sẻ logic xác thực dữ liệu hay các kiểu dữ liệu TypeScript giữa hai phía.

Thay vì chỉ tương tác với giao diện người dùng qua các Web API, Node.js cung cấp cho JavaScript khả năng làm việc với hệ thống tệp, mạng và các tiến trình hệ điều hành. Đây là nền tảng để xây dựng các hệ thống backend chịu tải cao và các ứng dụng thời gian thực.

Node.js đưa JavaScript ra khỏi trình duyệt để chạy trên máy chủ, hình ảnh chủ đề của bài viết

Node.js là gì?

Node.js không phải là một ngôn ngữ lập trình (ngôn ngữ thực thi vẫn là JavaScript), không phải là một framework (như Express hay NestJS) và cũng không phải là một web server đơn thuần. Nó là một runtime được xây dựng từ ba thành phần chính:

  1. V8 JavaScript Engine: Bộ máy do Google phát triển, không chỉ thông dịch mà còn sử dụng cơ chế JIT (Just-In-Time) compilation. V8 biên dịch trực tiếp JavaScript thành mã máy và tối ưu hóa các "hot code paths" (những đoạn mã chạy đi chạy lại nhiều lần) dựa trên runtime profiling, giúp mã thực thi với hiệu suất tiệm cận các ngôn ngữ biên dịch.
  2. Thư viện libuv: Một thư viện C chuyên trách quản lý Event loop và Thread pool. Quan trọng hơn, libuv cung cấp khả năng xử lý I/O bất đồng bộ ở cấp độ hệ điều hành (OS-level async I/O), cho phép Node.js xử lý hàng nghìn kết nối mà không bị treo luồng.
  3. Hệ thống API: Các module tích hợp sẵn để tương tác với File System, Networking và Child Processes.

Ba thành phần tạo nên Node.js: V8 engine, thư viện libuv và bộ module hệ thống

Node.js đóng gói JavaScript vào một môi trường có thể can thiệp sâu vào tài nguyên hệ thống, điều mà trình duyệt bị hạn chế nghiêm ngặt vì lý do bảo mật.

Node.js khác gì JavaScript chạy trong trình duyệt?

Dù cùng dùng chung cú pháp JavaScript, hệ sinh thái và API giữa hai môi trường này phân hóa rõ rệt. Trong trình duyệt, bạn làm việc với DOM, Window hoặc Cookies để thao tác trên UI. Node.js lược bỏ hoàn toàn các API này vì chúng vô nghĩa trên server. Thay vào đó, nó cung cấp các module như fs (truy cập tệp tin), http (giao thức mạng) hay streams (xử lý dữ liệu lớn).

So sánh hai môi trường: trình duyệt với DOM, Window, Cookies và Node.js với fs, http, streams cùng quyền truy cập hệ điều hành

Bạn hoàn toàn kiểm soát được môi trường chạy Node.js. Trong khi lập trình viên frontend thường phải dùng Babel để chuyển đổi mã về ES5 nhằm hỗ trợ các trình duyệt cũ, thì với Node.js, bạn chủ động chọn phiên bản runtime trên server. Điều này cho phép sử dụng các tính năng mới nhất của ECMAScript mà không cần thêm bước chuyển đổi phức tạp.

Về hệ thống module, Node.js hỗ trợ cả CommonJS (require) và ES Modules (import). require hoạt động đồng bộ, còn import của ES Modules là bất đồng bộ — một chi tiết quan trọng khi bạn thiết kế kiến trúc nạp module cho hệ thống.

Event loop và I/O không chặn hoạt động ra sao?

Node.js sử dụng mô hình đơn luồng (single-threaded) nhưng vẫn đạt thông lượng (throughput) cao nhờ cơ chế I/O không chặn (non-blocking I/O). Hãy hình dung qua phép ẩn dụ về "người bồi bàn": thay vì đứng đợi tại một bàn cho đến khi bếp nấu xong món, người bồi bàn (luồng chính) lấy yêu cầu, gửi vào bếp (libuv), rồi lập tức sang bàn khác phục vụ. Khi bếp làm xong, bồi bàn nhận được thông báo để mang món ra cho khách.

Vòng lặp sự kiện: luồng chính nhận yêu cầu, đẩy tác vụ nặng xuống thread pool của libuv rồi nhận kết quả qua callback

Event loop thực chất hoạt động qua nhiều giai đoạn (phases) như: timers (xử lý setTimeout), poll (nhận I/O mới) và check (xử lý setImmediate). Ngoài ra, các microtasks (như Promises) sẽ được giải quyết triệt để sau mỗi phase, trước khi vòng lặp chuyển sang phase tiếp theo.

Các tác vụ nặng như File I/O, Crypto hay Zlib sẽ được libuv đẩy xuống Thread pool (mặc định có 4 thread) để xử lý dưới nền, tránh làm nghẹt luồng chính. Một server HTTP tối giản trông như sau:

javascript
const http = require("http");
 
const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello World\n");
});
 
server.listen(8000, "127.0.0.1", () => {
  console.log("Server running at http://127.0.0.1:8000/");
});

Trong ví dụ này, hàm callback trong createServer chỉ được gọi khi có request tới, hoàn toàn không chặn các hoạt động khác của hệ thống.

npm và hệ sinh thái gói của Node.js

Trình quản lý gói npm đi kèm với Node.js hiện là kho lưu trữ phần mềm lớn nhất thế giới, với hơn 2,1 triệu gói tính đến năm 2024. Nó giúp tự động hóa toàn bộ chuỗi công cụ build và quản lý phụ thuộc (dependency), nhờ đó các đội phát triển giảm được chi phí làm MVP và rút ngắn thời gian đưa sản phẩm ra thị trường.

Nhưng một hệ sinh thái lớn cũng đi kèm rủi ro về chuỗi cung ứng. Hãy chạy npm audit thường xuyên để kiểm tra lỗ hổng bảo mật trong cây phụ thuộc. Với các dự án doanh nghiệp, kiểm soát gói bên thứ ba bằng lockfile và quét lỗ hổng là quy trình bắt buộc để thu hẹp bề mặt tấn công của hệ thống.

Khi nào nên dùng Node.js — và khi nào không?

Việc chọn Node.js cần dựa trên bản chất của tải công việc (workload):

  • Trường hợp nên dùng: Các tác vụ hướng I/O (I/O-bound) là nơi Node.js thể hiện hiệu quả cao nhất. Ứng dụng chat, streaming, game online hoặc kiến trúc microservices cần xử lý hàng nghìn kết nối đồng thời với độ trễ thấp là kịch bản lý tưởng. Về hiệu suất, Node.js có thể đạt throughput khoảng 30.000 req/s trong điều kiện I/O-bound, vượt xa con số ~5.000 req/s của Python/Django.
  • Trường hợp KHÔNG nên dùng: Các tác vụ tính toán nặng (CPU-bound) như xử lý video, mã hóa dữ liệu phức tạp hoặc AI/ML. Do tính chất đơn luồng, một vòng lặp tính toán nặng sẽ chiếm dụng call stack, làm "đứng" Event loop và chặn đứng mọi yêu cầu khác của người dùng.

Hai loại tải: tác vụ hướng I/O mà Node.js xử lý rất tốt, và tác vụ nặng CPU làm nghẽn vòng lặp sự kiện

Từ phiên bản v12, Node.js đã có Worker Threads để xử lý đa luồng cho các tác vụ CPU-bound, nhưng giải pháp này buộc bạn phải cân nhắc chi phí truyền dữ liệu giữa các thread (serialization/deserialization).

Nên bắt đầu với phiên bản nào?

Trong môi trường thực tế (production), bạn hãy luôn ưu tiên phiên bản LTS (Long Term Support — hỗ trợ dài hạn). Chu kỳ hỗ trợ của một phiên bản LTS kéo dài 30 tháng (18 tháng Active và 12 tháng Maintenance), đủ để hệ thống của bạn nhận các bản vá bảo mật mà không bị cuốn theo những thay đổi lớn về API.

Để quản lý nhiều dự án với các phiên bản Node.js khác nhau, công cụ chuẩn là NVM (Node Version Manager) — nó giúp bạn chuyển đổi môi trường tức thì mà không gây xung đột hệ thống. Lệnh cài phiên bản LTS mới nhất:

bash
nvm install --lts

Tài liệu tham khảo

Chia sẻ bài viết

X / TwitterFacebookLinkedIn