jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Source Maps — How Debugging Survives the Build Process

A bilingual deep-dive into source maps: what they are, how they work internally (VLQ encoding, mappings field), how to generate and configure them, and best practices for production debugging.

Vấn đề Source Map giải quyết

Ứng dụng web hiện đại không gửi code bạn viết. Giữa source và trình duyệt, code của bạn đi qua:

Your Code                      What Browser Receives
{Code của bạn}                 {Trình duyệt nhận}

TypeScript     ─┐
SCSS/PostCSS   ─┤── Build ──→  Minified JS bundle
JSX/TSX        ─┤   Tools      Compressed CSS
Multiple files ─┘              Single concatenated files
                               Unreadable variable names

Khi có lỗi ở production, lỗi trỏ đến app.min.js:1:34892 — một dòng có 50.000 ký tự. Hoàn toàn vô dụng cho debug.

Source map nối liền khoảng cách này. Chúng ánh xạ code đã biên dịch ngược về source gốc, để DevTools có thể hiện file TypeScript, dòng chính xác, tên biến gốc.


Source Map là gì

Source map là file JSON chứa thông tin ánh xạ:

{
  "version": 3,
  "file": "app.min.js",
  "sources": ["src/utils.ts", "src/main.ts", "src/components/App.tsx"],
  "sourcesContent": ["export function add(a: number...", "import { App }...", "..."],
  "names": ["add", "result", "handleClick", "useState"],
  "mappings": "AAAA,SAAS,IAAI,EAAE,CAAS..."
}
FieldMục đích
versionLuôn là 3 (phiên bản spec hiện tại)
fileFile được tạo mà map này thuộc về
sourcesMảng đường dẫn file source gốc
sourcesContentCode source gốc (tuỳ chọn, cho phép debug mà không cần serve source)
namesMảng định danh gốc (tên biến/hàm trước khi minify)
mappingsDữ liệu ánh xạ mã hoá VLQ

Cách Mapping hoạt động

Trường mappings là trái tim của source map. Nó dùng mã hoá VLQ Base64 để nén dữ liệu vị trí.

Mã hoá

Mỗi đoạn trong chuỗi mappings đại diện cho vị trí trong code được tạo và nó đến từ đâu:

Mappings: "AAAA,SAAS,IAAI;AACA,SAAS..."
           │          │
           │          └── Semicolons separate lines in generated file
           │              {Chấm phẩy phân tách dòng trong file được tạo}

           └── Commas separate segments within a line
               {Phẩy phân tách các đoạn trong một dòng}

Mỗi đoạn giải mã thành 4 hoặc 5 giá trị:

Segment: AAAA

Decoded: [0, 0, 0, 0]
          │  │  │  │
          │  │  │  └── Column in original source
          │  │  │      {Cột trong source gốc}
          │  │  │
          │  │  └── Line in original source (relative)
          │  │      {Dòng trong source gốc (tương đối)}
          │  │
          │  └── Index into "sources" array
          │      {Index trong mảng "sources"}

          └── Column in generated file (relative)
              {Cột trong file được tạo (tương đối)}

Optional 5th value: index into "names" array
{Giá trị thứ 5 tuỳ chọn: index trong mảng "names"}

Nhận thức quan trọng: tất cả giá trị đều tương đối so với đoạn trước. Điều này khiến mã hoá VLQ rất nhỏ gọn vì hầu hết thay đổi giữa các token liền kề là số nhỏ.

VLQ (Đại lượng độ dài biến đổi)

VLQ mã hoá số nguyên dùng nhóm 6-bit, bit cao nhất chỉ “còn nữa”:

Number: 16
Binary: 10000

VLQ encoding steps:
{Các bước mã hoá VLQ:}
1. Take binary: 10000
2. Add sign bit (positive = 0): 100000
3. Split into 5-bit groups: 00001 | 00000
4. Reverse groups: 00000 | 00001
5. Add continuation bit: 100000 | 000001
6. Base64 encode each group: g | B

Result: "gB"

Đây là lý do source map nhỏ hơn nhiều so với bạn nghĩ — hầu hết mapping mã hoá chỉ 4-8 ký tự Base64 mỗi token.


Tạo Source Map

Vite

// vite.config.ts
export default defineConfig({
  build: {
    sourcemap: true, // generates .map files for production
  },
  css: {
    devSourcemap: true, // CSS source maps in dev mode
  },
});

Webpack

// webpack.config.js
module.exports = {
  // Development — fast rebuild, full mapping
  devtool: 'eval-source-map',

  // Production — separate .map files, full mapping
  // devtool: 'source-map',

  // Production — .map without sourcesContent (smaller, needs source files)
  // devtool: 'nosources-source-map',
};

TypeScript

{
  "compilerOptions": {
    "sourceMap": true,
    "declarationMap": true,
    "inlineSources": true
  }
}

esbuild

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  sourcemap: true, // external .map file
  // sourcemap: 'inline', // embedded in output
  // sourcemap: 'linked', // external with //# sourceMappingURL comment
});

Các loại Source Map

LoạiCách hoạt độngTrường hợp dùng
ExternalFile .map riêng, liên kết qua commentProduction (chỉ tải khi DevTools mở)
InlineMã hoá Base64 bên trong file JS/CSSDevelopment (không cần request thêm)
Hidden.map File .map tồn tại nhưng không có comment liên kếtProduction nơi bạn chỉ upload map cho error tracking
NosourcesFile map không có sourcesContentProduction nơi bạn không muốn lộ source code

Cách trình duyệt dùng Source Map

Phát hiện

Trình duyệt tìm source map qua một comment đặc biệt ở dòng cuối của file đã biên dịch:

// app.min.js (compiled)
(() => { /* ...minified code... */ })();

Dòng cuối là comment chú thích //# sourceMappingURL=app.min.js.map — DevTools đọc nó để tìm map.

Hoặc qua HTTP header:

SourceMap: /path/to/app.min.js.map

DevTools làm gì

  1. Phát hiện sourceMappingURL
  2. Tải file .map
  3. Phân tích mappings
  4. Hiển thị file gốc trong panel Sources
  5. Ánh xạ stack trace lỗi về dòng gốc
  6. Cho phép đặt breakpoint trong source gốc

Thực hành tốt cho Production

Đừng lộ Source Map công khai

Source map chứa toàn bộ code source gốc Trong production:

# Nginx: block access to .map files from public
location ~* \.map$ {
  deny all;
  return 404;
}
// Or: use hidden source maps + upload to error tracking
// vite.config.ts
export default defineConfig({
  build: {
    sourcemap: 'hidden', // no sourceMappingURL comment in output
  },
});

Upload lên dịch vụ Error Tracking

Các dịch vụ như Sentry, Datadog, Bugsnag có thể dùng source map để giải mã stack trace lỗi mà không lộ chúng công khai:

# Example: upload source maps to Sentry after build
sentry-cli sourcemaps upload \
  --org my-org \
  --project my-project \
  --release "1.0.0" \
  ./dist/assets/

Cân nhắc kích thước Source Map

Source map có thể lớn gấp 3-5 lần file đã biên dịch:

FileĐã biên dịchSource Map
app.min.js150KB450KB - 750KB
styles.min.css30KB90KB - 150KB

Vì chúng chỉ được tải khi DevTools mở, điều này không ảnh hưởng performance người dùng. Nhưng cân nhắc chi phí lưu trữ và băng thông CDN cho upload error tracking.


Source Map CSS

Source map CSS hoạt động giống hệt nhưng ánh xạ CSS đã biên dịch về source SCSS/PostCSS/Tailwind:

/* styles.min.css */
.nav{display:flex;gap:.5rem}.nav a{color:#c8ff00}

CSS đã biên dịch kết thúc bằng /*# sourceMappingURL=styles.min.css.map */ — dạng CSS của cùng comment chú thích.

Trong DevTools: click bất kỳ rule style → nhảy đến file .scss gốc, đúng dòng.

Bật Source Map CSS

// vite.config.ts
export default defineConfig({
  css: {
    devSourcemap: true,
  },
});
// PostCSS
module.exports = {
  map: { inline: false }, // external .map file
};
// Sass (CLI)
// sass --source-map input.scss output.css

Debug với Source Map

Mẹo Chrome DevTools

  1. Panel Sources: file gốc xuất hiện trong cây file
  2. Breakpoint: đặt trong source gốc — chúng hoạt động trong code đã biên dịch
  3. Lỗi Console: stack trace hiển thị file:dòng gốc
  4. Blackboxing: để bỏ qua code thư viện khi step

Extension x_google_ignoreList

Bundler hiện đại thêm extension này để cho DevTools biết file nào là “code thư viện”:

{
  "version": 3,
  "mappings": "...",
  "sources": ["node_modules/react/index.js", "src/App.tsx", "src/utils.ts"],
  "x_google_ignoreList": [0]
}

sẽ tự động ẩn trong panel Sources và bị bỏ qua khi step qua code.


Đề xuất Source Map v4

Spec v3 hiện tại có hạn chế:

  • Mất tên biến: khi biến bị tối ưu hoá, bạn không thể inspect
  • Không có thông tin scope: không phân biệt được biến cùng tên trong scope khác nhau
  • Không ánh xạ biểu thức: biểu thức inline mất ngữ cảnh

Đề xuất v4 thêm:

  • Thông tin scope
  • Ràng buộc biến gốc
  • Theo dõi biểu thức tốt hơn

Tham khảo nhanh

Khi nào dùng loại nào

Môi trườngCấu hình Source MapTại sao
Local developmentinline or eval-source-mapRebuild nhanh nhất, không request thêm
Staging/QAsource-map (external)Khả năng debug đầy đủ
Production (public)hidden + upload to error trackingBảo mật: không lộ source
Production (internal)source-map + restrict accessDebug đầy đủ cho team

Khắc phục sự cố

Vấn đềNguyên nhânSửa
DevTools hiện code minifiedMissing sourceMappingURL commentCheck build config generates maps
”Could not load content”Map file URL is wrong or CORS-blockedCheck path, add CORS headers
Breakpoint không kích hoạtMap is outdated (stale build)Rebuild, clear browser cache
Variables show as undefinedBị tối ưu hoá lúc biên dịchReduce optimization level or use v4 when available