Source Map คืออะไร และตั้งค่าอย่างไรใน Production
วิธีตั้งค่าและใช้งาน Source Maps สำหรับ Production — ครบทุก Edge Case ที่เจอจริง
1. Source Map คืออะไร
เมื่อ build TypeScript หรือ bundle โค้ด ไฟล์ต้นฉบับ .ts จะถูกแปลงเป็น .js ที่อ่านยาก โดยเฉพาะเมื่อ minify แล้ว
Source map คือไฟล์ .map ที่เก็บข้อมูล mapping ระหว่างโค้ดที่ build แล้วกับโค้ดต้นฉบับ ทำให้ tools ต่างๆ สามารถแปลง position กลับมาเป็นบรรทัดจริงๆ ใน .ts ได้
ทำไมถึงต้องใช้
ไม่มี source map:
TypeError: Cannot read property 'id' of undefined
at t.getUserById (dist/index.js:1:2345) ← อ่านไม่ออก ไม่รู้ไปแก้ที่ไหน
มี source map:
TypeError: Cannot read property 'id' of undefined
at getUserById (src/auth/service.ts:42:8) ← รู้ทันทีว่าต้องไปดูไฟล์ไหน บรรทัดไหน
ใช้ได้ที่ไหนบ้าง
| ที่ | วิธีใช้ |
|---|---|
| Node.js | node --enable-source-maps dist/app.js |
| Browser DevTools | เปิดอัตโนมัติเมื่อมีไฟล์ .map |
| Error Monitoring | Sentry, Datadog, Rollbar — upload .map แล้ว decode stack trace อัตโนมัติ |
| IDE Debugger | VS Code อ่าน .map ให้ set breakpoint ใน .ts ได้โดยตรง |
ไฟล์ที่เกี่ยวข้อง
dist/
├── index.js ← โค้ดที่รันจริง (compiled/minified)
└── index.js.map ← source map (mapping กลับไปหา .ts)
ใน .js จะมี comment บอกว่ามี .map อยู่:
//# sourceMappingURL=index.js.map
Source Map เป็นส่วนหนึ่งของ TypeScript Production Build Strategy — ซึ่งครอบคลุมตั้งแต่การ compile, bundle, minify ไปจนถึง deploy และ observability บทความนี้เจาะเฉพาะ source map แต่ทุก decision ที่เลือกมีผลต่อ debugging experience ใน production โดยตรง
2. ประเภทของ Source Map
Source map มี 2 มิติ ที่ต้องตั้งค่าแยกกัน
มิติที่ 1 — ที่เก็บไฟล์ .map (ตั้งที่ bundler)
| แบบ | .js มี comment? | .map แยกไฟล์? | อธิบาย |
|---|---|---|---|
linked | ✅ มี | ✅ มี | default ทุก tool — Node.js หา .map ได้อัตโนมัติ |
inline | embed อยู่ใน .js | ❌ ไม่มี | .js บวมมาก |
external | ❌ ไม่มี | ✅ มี | Node.js หาเองไม่ได้ |
both | ✅ มี + embed | ✅ มี | ซ้ำซ้อน ไม่ค่อยใช้ |
ตัวอย่าง linked vs external:
// linked — Node.js หา .map ได้
function getUserById(id) { return db.query(id) }
//# sourceMappingURL=index.js.map ← comment นี้คือสัญญาณ
// external — Node.js ไม่รู้ว่ามี .map อยู่
function getUserById(id) { return db.query(id) }
// (ไม่มีอะไรเลย)
มิติที่ 2 — Content ใน .map (ตั้งที่ tsconfig)
| แบบ | sourcesContent | อธิบาย |
|---|---|---|
| ปกติ | null | .map อ้าง path ไปหาไฟล์ .ts บน disk |
inlineSources: true | embed .ts ไว้ใน .map | ไม่ต้องหาไฟล์ .ts เลย |
ทั้ง 2 มิติใช้ร่วมกันได้ทุกแบบ — เช่น linked + inlineSources หรือ inline + ไม่มี inlineSources
3. ตั้งค่าในแต่ละ Tool
tsconfig.json
{
"compilerOptions": {
"sourceMap": true, // linked (default)
"inlineSourceMap": true, // inline
"inlineSources": true // embed .ts ใน .map (มิติที่ 2)
}
}
sourceMapกับinlineSourceMapใช้พร้อมกันไม่ได้ — ต้องเลือกอย่างใดอย่างหนึ่ง
esbuild
esbuild.build({
sourcemap: true, // linked
sourcemap: 'inline', // inline
sourcemap: 'external', // external
sourcemap: 'both', // both
})
tsup
// tsup.config.ts
export default {
sourcemap: true, // linked
sourcemap: 'inline', // inline
// external หรือ both → ใช้ผ่าน esbuildOptions
esbuildOptions(options) {
options.sourcemap = 'external'
}
}
เปิดใช้ใน Node.js
--enable-source-maps ไม่ได้เปิดเป็น default แม้ใน Node.js 22 — ต้อง opt-in เองเสมอ เหตุผลคือมี performance overhead เมื่อ bundle ใหญ่และมี error stack ถูกเข้าถึงบ่อยๆ
⚠️ ต้องใช้
--enable-source-mapsเสมอเมื่อรัน.jsไม่ว่าจะเป็น dev หรือ production
- รัน
.tsโดยตรง (tsx, ts-node) → ไม่ต้องใช้ flag เพราะไม่มี.jsและ.mapเกิดขึ้นเลย- รัน
.jsที่ build แล้ว → ต้องใช้ flag เสมอ ไม่ว่าจะ dev หรือ production
# วิธีที่ 1 — ใส่ flag ตรงๆ
node --enable-source-maps dist/app.js
# วิธีที่ 2 — ใช้ NODE_OPTIONS (แนะนำ)
NODE_OPTIONS="--enable-source-maps" node dist/app.js
ข้อดีของ NODE_OPTIONS คือ propagate ไปยัง child process อัตโนมัติ ไม่ต้องใส่ flag ซ้ำทุกที่
// package.json
{
"scripts": {
"start": "NODE_OPTIONS=\"--enable-source-maps\" node dist/app.js"
}
}
Fastify CLI + NODE_OPTIONS
fastify CLI ไม่ได้ set --enable-source-maps ให้อัตโนมัติ → ต้องตั้งเองผ่าน NODE_OPTIONS:
// package.json
{
"scripts": {
"start": "NODE_OPTIONS=\"--enable-source-maps\" fastify start -l info dist/app.js"
}
}
หรือถ้าตั้งใน .env หรือ Docker environment variable แทน script ก็ไม่ต้องแก้ package.json เลย:
# .env — ใส่ quote ไว้เสมอ รองรับการเพิ่ม flag ในอนาคต
NODE_OPTIONS="--enable-source-maps"
# Dockerfile — ไม่ต้องมี quote
ENV NODE_OPTIONS=--enable-source-maps
4. pnpm Workspace + Symlink
โครงสร้าง
monorepo/
├── packages/
│ └── my-lib/
│ ├── src/
│ │ └── auth/index.ts
│ ├── dist/ ← Node.js โหลดจากตรงนี้
│ └── package.json
└── apps/
└── my-app/
├── src/
├── node_modules/
│ └── my-lib → symlink → ../../packages/my-lib
└── package.json
// my-app/package.json
{
"dependencies": {
"my-lib": "workspace:*"
}
}
pnpm สร้าง symlink แทนการ copy ไฟล์ จึงเร็วและประหยัดพื้นที่
5. ปัญหา Symlink + Source Map
สาเหตุ
bundler ตอน build my-app เห็น my-lib ผ่าน symlink จึง generate path ใน .map แบบนี้:
{
"sources": ["../../node_modules/my-lib/src/auth/index.ts"]
}
ตอน deploy บน server — copy เฉพาะ dist/ ไม่ได้ copy src/ ไปด้วย:
server/
└── dist/
└── app.js ✅ มี
└── app.js.map ✅ มี
❌ node_modules/my-lib/src/ ไม่มี!
ผลคือ Node.js หาไฟล์ .ts ต้นฉบับไม่เจอ — stack trace ยังอ้างถึง .js ใน dist/
6. วิธีแก้ปัญหา Symlink
วิธีที่ 1 — inlineSources: true (แนะนำ)
// my-lib/tsconfig.json หรือ tsup.config.ts ที่ตั้ง tsconfig
{
"compilerOptions": {
"sourceMap": true,
"inlineSources": true // embed .ts ไว้ใน .map เลย
}
}
embed .ts ไว้ใน .map → ไม่ต้องหาไฟล์จาก disk → ไม่สนใจ symlink หรือ path บน server
✅ deploy เฉพาะ dist/ ก็พอ
✅ แก้ปัญหา path alias และ symlink ได้ทุกกรณี
❌ .map ใหญ่ขึ้น (มี .ts content อยู่ภายใน)
วิธีที่ 2 — ship src/ ไปด้วย
// my-lib/package.json
{
"files": ["dist", "src"]
}
relative path จาก dist/ ไปหา src/ ถูกต้องเสมอเพราะ structure ภายใน package ไม่เปลี่ยน
✅ .map เล็กกว่า (ไม่ต้อง embed)
✅ path alias ไม่มีปัญหา (bundler resolve alias → path จริงก่อน generate .map)
❌ package/deploy size ใหญ่ขึ้น
❌ เหมาะสำหรับ internal package เท่านั้น (ไม่ควร publish src/ ขึ้น npm)
เปรียบเทียบขนาดไฟล์
แบบ inlineSources:
dist/auth/index.js 100 KB
dist/auth/index.js.map 350 KB ← embed .ts ไว้แล้ว
รวม deploy 450 KB
แบบ ship src/:
dist/auth/index.js 100 KB
dist/auth/index.js.map 80 KB ← .map เล็กกว่า ไม่ embed
src/auth/index.ts 300 KB ← ไฟล์ .ts จริงๆ
รวม deploy 480 KB
ต่างกันนิดเดียว บางกรณี
inlineSourcesอาจเล็กกว่าด้วยซ้ำ ขึ้นอยู่กับขนาด src จริงๆ
7. Source Map + Minify
ผลต่อขนาดไฟล์
minify + inlineSources:
dist/index.js 100 KB ← minified ✅
dist/index.js.map 350 KB ← sourcesContent ไม่ถูก minify
ไม่ minify + inlineSources:
dist/index.js 300 KB
dist/index.js.map 400 KB ← .map ใหญ่กว่าเล็กน้อย (mappings ซับซ้อนน้อยกว่า)
สรุป
| ส่วน | minify มีผล? | อธิบาย |
|---|---|---|
.js | ✅ เล็กลงมาก | rename vars, remove whitespace |
.map ส่วน mappings | ✅ เล็กขึ้นนิดหน่อย | แต่ minify ทำให้ซับซ้อนขึ้น |
.map ส่วน sourcesContent | ❌ ไม่มีผล | เก็บ .ts ต้นฉบับไม่เปลี่ยน |
ผลรวม: minify ทำให้ .js เล็กลง แต่ .map แทบไม่เปลี่ยน
8. Source Map + OpenTelemetry
OTel ให้ข้อมูล flow, trace, span ได้ดี แต่ไม่สามารถแก้ปัญหา stack trace ที่อ่านไม่ออกได้
bundle + minify + ไม่มี source map:
Error at dist/index.js:1:2345 ← อ่านไม่ออก แม้มี OTel
bundle + minify + มี source map:
Error at src/auth/service.ts:42:8 ← อ่านออก ✅
| กรณี | source map จำเป็น? |
|---|---|
| ไม่ minify + มี OTel | nice to have |
| bundle + minify | ✅ ต้องมีเสมอ |
| minify + ไม่มี OTel | ✅ ต้องมีมากๆ |
9. Config สรุป (Production)
Local Package (my-lib)
// tsconfig.prod.json
{
"compilerOptions": {
"outDir": "./dist",
"sourceMap": true,
"inlineSources": true,
"declaration": false
}
}
// tsup.config.ts
export default {
entry: {
auth: 'src/auth/index.ts',
db: 'src/db/index.ts',
},
minify: true,
sourcemap: true, // linked (default)
tsconfig: 'tsconfig.prod.json',
}
Fastify App (my-app)
// tsconfig.prod.json
{
"compilerOptions": {
"sourceMap": true,
"inlineSources": true
}
}
// package.json
{
"scripts": {
"start": "node --enable-source-maps dist/app.js"
}
}
Deploy Checklist
✅ build my-lib ก่อนเสมอ
✅ build my-app หลัง
✅ sourceMap: true (ทั้ง lib และ app)
✅ inlineSources: true (แก้ปัญหา symlink + path บน server)
✅ sourcemap: true (tsup) (linked — Node.js หา .map ได้อัตโนมัติ)
✅ NODE_OPTIONS="--enable-source-maps" (ตั้งใน .env หรือ package.json scripts)
✅ ship เฉพาะ dist/ (ไม่ต้อง ship src/)
FAQ
Q: sourceMap: true กับ inlineSourceMap: true ใน tsconfig ต่างกันอย่างไร?
sourceMap: true สร้างไฟล์ .map แยกต่างหาก และเพิ่ม comment //# sourceMappingURL= ท้าย .js (แบบ linked) ส่วน inlineSourceMap: true embed .map ทั้งก้อนลงใน .js เลย ทำให้ไฟล์ .js ใหญ่มาก ทั้งสองใช้พร้อมกันไม่ได้ สำหรับ production แนะนำ sourceMap: true (linked) เสมอ
Q: ควรใช้ inlineSources: true เมื่อไหร่ และควรหลีกเลี่ยงเมื่อไหร่?
ใช้เมื่อ deploy โดยไม่ได้ copy src/ ไปบน server เช่น กรณี pnpm workspace symlink หรือ Docker image ที่ copy เฉพาะ dist/ — inlineSources embed .ts ต้นฉบับลงใน .map ทำให้ไม่ต้องหาไฟล์จาก disk เลย ควรหลีกเลี่ยงถ้า source มี sensitive business logic ที่ไม่อยากให้ติดไปใน deploy artifact
Q: ทำไม Node.js ถึงต้องใช้ --enable-source-maps ทั้งที่มีไฟล์ .map อยู่แล้ว?
Node.js ไม่ได้อ่าน source map โดยอัตโนมัติเพื่อประหยัด overhead ตอน runtime — ต้อง opt-in ด้วย --enable-source-maps เพื่อให้ stack trace ถูก remap เมื่อเกิด error flag นี้มี overhead เล็กน้อยแต่คุ้มค่ามากใน production โดยเฉพาะเมื่อ minify
Q: Source Map เกี่ยวข้องกับ OpenTelemetry อย่างไร และทำงานเสริมกันได้แค่ไหน?
OTel และ source map แก้ปัญหาคนละมิติ — OTel บอก ที่ไหนใน flow ที่เกิด error (trace, span, context) แต่ไม่สามารถแปล stack trace ที่ minify แล้วได้ ส่วน source map แก้ปัญหา บรรทัดไหนใน source code ทั้งสองต้องทำงานร่วมกัน: OTel บอก context, source map บอก exact location
Q: ข้อผิดพลาดที่พบบ่อยเมื่อตั้งค่า source map สำหรับ production มีอะไรบ้าง?
ข้อผิดพลาดที่พบบ่อยคือ ใช้ sourcemap: 'external' ใน bundler โดยไม่รู้ว่า Node.js จะหา .map ไม่เจอ (ต้องใช้ linked หรือ inline), ลืมใส่ --enable-source-maps ตอน run, และใช้ inlineSources: true แต่ลืมตั้งที่ my-lib ด้วย ทำให้ app source map ถูก remap แต่ error ใน lib ยังอ้าง path ที่ไม่มีบน server