jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Vite · Part 3 — The Dev Server & Native ESM

How the dev server actually works: serving source over native ESM, rewriting bare and relative imports into fetchable URLs, transforming each file on request, and why navigation only fetches the new modules. With an on-demand request demo.

In Part 1 we said the browser does the bundling in dev {Ở Phần 1 ta nói trình duyệt tự ghép khi dev}. But native ESM has rules the browser enforces — and Vite’s dev server exists to bridge the gap between your source and what the browser can actually run {Nhưng ESM native có luật trình duyệt áp đặt — và dev server của Vite tồn tại để bắc cầu giữa source của bạnthứ trình duyệt thật sự chạy được}.

Click through a route load and watch the requests appear one by one {Bấm qua một lần tải route và xem các request hiện ra từng cái}:


1. The browser’s rule {Luật của trình duyệt}

Native ESM only accepts imports that are valid URLs — absolute (/src/x.js) or relative (./x.js) {ESM native chỉ chấp nhận import là URL hợp lệ — tuyệt đối (/src/x.js) hoặc tương đối (./x.js)}. But your code is full of bare imports like import React from "react" {Nhưng code của bạn đầy bare import như import React from "react"}. The browser has no idea where "react" lives {Trình duyệt không biết "react" ở đâu}.

Vite intercepts every module request and rewrites the imports before sending the file {Vite chặn mọi request module và viết lại import trước khi gửi file}:

// you wrote
import { useState } from "react";
import Home from "./components/Home";

// Vite serves (rewritten)
import { useState } from "/node_modules/.vite/deps/react.js";
import Home from "/src/components/Home.tsx";

Bare specifiers become real URLs (pointing at pre-bundled deps — Part 4), and relative imports get their extension resolved {Bare specifier thành URL thật (trỏ tới dep đã pre-bundle — Phần 4), và import tương đối được phân giải đuôi}.


2. Transform on request {Biến đổi theo yêu cầu}

When the browser requests /src/App.tsx, Vite {Khi trình duyệt yêu cầu /src/App.tsx, Vite}:

  1. Reads the file {Đọc file}.
  2. Transforms it with Oxc (TS→JS, JSX→JS) — fast, Rust-based {Biến đổi bằng Oxc (TS→JS, JSX→JS) — nhanh, viết bằng Rust}.
  3. Rewrites its imports {Viết lại import của nó}.
  4. Sends valid ESM the browser can execute {Gửi ESM hợp lệ trình duyệt thực thi được}.

Crucially, this happens per file, on demand {Quan trọng: việc này xảy ra từng file, theo yêu cầu}. A file no route touches is never read or transformed {File không route nào chạm tới không bao giờ được đọc hay biến đổi}.

Vite does not type-check during transform — Oxc just strips types {Vite không kiểm tra kiểu khi biến đổi — Oxc chỉ bóc kiểu đi}. Run tsc --noEmit (or vue-tsc) separately in CI for type safety {Chạy tsc --noEmit (hoặc vue-tsc) riêng trong CI để an toàn kiểu}.


3. The waterfall, and why it’s fine {Thác nước, và vì sao ổn}

Native ESM means nested imports create a request waterfall: main.tsx imports App.tsx, which imports Home.tsx… {ESM native nghĩa là import lồng tạo thác nước request: main.tsx import App.tsx, import Home.tsx…}. On a real network that would be slow, but in dev everything is local and HTTP/2-multiplexed, so it’s near-instant {Trên mạng thật sẽ chậm, nhưng khi dev mọi thứ cục bộ và HTTP/2 ghép kênh, nên gần như tức thì}.

To avoid a deep waterfall for dependencies, Vite pre-bundles them into single files (Part 4) — that’s why react is one request, not hundreds {Để tránh thác nước sâu cho dependency, Vite pre-bundle chúng thành file đơn (Phần 4) — đó là vì sao react là một request, không phải hàng trăm}.


4. Navigation only fetches what’s new {Điều hướng chỉ tải cái mới}

This is the payoff {Đây là phần thưởng}. When you navigate to /dashboard, the browser requests only the modules that route addsDashboard.tsx, Chart.tsx {Khi điều hướng tới /dashboard, trình duyệt chỉ yêu cầu module route đó thêm vàoDashboard.tsx, Chart.tsx}. Already-loaded modules are cached; nothing is rebuilt {Module đã tải được cache; không gì bị build lại}. Compare that to a bundler re-running on graph changes {So với bundler chạy lại khi đồ thị đổi}.

In the demo, load home then navigate — note the request count only ticks up by the two new files {Trong demo, tải home rồi điều hướng — để ý số request chỉ tăng đúng hai file mới}.


5. The 304 and the module cache {304 và cache module}

Vite sends strong caching headers {Vite gửi header cache mạnh}:

  • Source files304 Not Modified when unchanged (revalidated) {File source304 Not Modified khi không đổi (revalidate)}.
  • Pre-bundled depsCache-Control: max-age=31536000, immutable (hashed, cached hard) {Dep đã pre-bundle → cache cứng (đã hash)}.

So a page reload re-requests source modules but they return instantly as 304s, and deps aren’t re-requested at all {Nên reload trang sẽ re-request module source nhưng trả về tức thì dạng 304, còn dep không bị re-request}.


6. Exercises {Bài tập}

1. Why can’t the browser run import React from "react" directly, and what does Vite do about it? {Vì sao trình duyệt không chạy trực tiếp import React from "react", và Vite làm gì?}

Solution {Lời giải}

Native ESM requires a valid URL, not a bare specifier. Vite rewrites "react" to a real URL pointing at the pre-bundled dependency {ESM native cần URL hợp lệ, không phải bare specifier. Vite viết lại "react" thành URL thật trỏ tới dependency đã pre-bundle}.

2. A teammate says “Vite doesn’t catch my TypeScript errors in dev.” Are they right, and why? {Đồng đội nói “Vite không bắt lỗi TypeScript khi dev.” Đúng không, vì sao?}

Solution {Lời giải}

Right — Vite/Oxc only strips types for speed; it doesn’t type-check. Run tsc --noEmit separately (editor + CI) {Đúng — Vite/Oxc chỉ bóc kiểu cho nhanh; không kiểm tra kiểu. Chạy tsc --noEmit riêng (editor + CI)}.

3. Why does navigating to a new route in dev feel instant compared to a bundler? {Vì sao điều hướng tới route mới khi dev cảm giác tức thì so với bundler?}

Solution {Lời giải}

Vite only transforms and serves the new modules that route imports; everything else is already cached and nothing is re-bundled {Vite chỉ biến đổi và phục vụ module mới route đó import; phần còn lại đã cache và không gì bị bundle lại}.

Stretch {Nâng cao}: in the demo, load home, then dashboard, then reset and reload home — observe that deps (react) come from the pre-bundle, not your source tree {trong demo, tải home, rồi dashboard, rồi reset và tải lại home — để ý dep (react) đến từ pre-bundle, không phải cây source của bạn}.


Key takeaways {Điểm chính}

  • The browser only runs valid-URL imports; Vite rewrites bare and relative imports {Trình duyệt chỉ chạy import URL hợp lệ; Vite viết lại bare và relative import}.
  • Each file is transformed on request with Oxc — no type-checking {Mỗi file được biến đổi theo yêu cầu bằng Oxc — không kiểm tra kiểu}.
  • Dev’s import waterfall is fine because it’s local + multiplexed, and deps are pre-bundled {Thác nước import khi dev ổn vì cục bộ + ghép kênh, và dep được pre-bundle}.
  • Navigation fetches only new modules — nothing is re-bundled {Điều hướng chỉ tải module mới — không gì bị bundle lại}.
  • Strong caching (304 + immutable deps) keeps reloads fast {Cache mạnh (304 + dep immutable) giữ reload nhanh}.

Next up {Tiếp theo}

Part 4 — Dependency pre-bundling: why node_modules gets pre-bundled into .vite/deps, how Vite converts CommonJS to ESM, and when the cache invalidates {Phần 4 — Pre-bundle dependency: vì sao node_modules được pre-bundle vào .vite/deps, cách Vite chuyển CommonJS sang ESM, và khi nào cache mất hiệu lực}.