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..."
}
| Field | Mục đích |
|---|---|
version | Luôn là 3 (phiên bản spec hiện tại) |
file | File được tạo mà map này thuộc về |
sources | Mảng đường dẫn file source gốc |
sourcesContent | Code source gốc (tuỳ chọn, cho phép debug mà không cần serve source) |
names | Mảng định danh gốc (tên biến/hàm trước khi minify) |
mappings | Dữ 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ại | Cách hoạt động | Trường hợp dùng |
|---|---|---|
| External | File .map riêng, liên kết qua comment | Production (chỉ tải khi DevTools mở) |
| Inline | Mã hoá Base64 bên trong file JS/CSS | Development (không cần request thêm) |
| Hidden | .map File .map tồn tại nhưng không có comment liên kết | Production nơi bạn chỉ upload map cho error tracking |
| Nosources | File map không có sourcesContent | Production 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ì
- Phát hiện
sourceMappingURL - Tải file
.map - Phân tích mappings
- Hiển thị file gốc trong panel Sources
- Ánh xạ stack trace lỗi về dòng gốc
- 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ịch | Source Map |
|---|---|---|
app.min.js | 150KB | 450KB - 750KB |
styles.min.css | 30KB | 90KB - 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
- Panel Sources: file gốc xuất hiện trong cây file
- Breakpoint: đặt trong source gốc — chúng hoạt động trong code đã biên dịch
- Lỗi Console: stack trace hiển thị file:dòng gốc
- 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ường | Cấu hình Source Map | Tại sao |
|---|---|---|
| Local development | inline or eval-source-map | Rebuild nhanh nhất, không request thêm |
| Staging/QA | source-map (external) | Khả năng debug đầy đủ |
| Production (public) | hidden + upload to error tracking | Bảo mật: không lộ source |
| Production (internal) | source-map + restrict access | Debug đầy đủ cho team |
Khắc phục sự cố
| Vấn đề | Nguyên nhân | Sửa |
|---|---|---|
| DevTools hiện code minified | Missing sourceMappingURL comment | Check build config generates maps |
| ”Could not load content” | Map file URL is wrong or CORS-blocked | Check path, add CORS headers |
| Breakpoint không kích hoạt | Map is outdated (stale build) | Rebuild, clear browser cache |
Variables show as undefined | Bị tối ưu hoá lúc biên dịch | Reduce optimization level or use v4 when available |