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.jsnode --enable-source-maps dist/app.js
Browser DevToolsเปิดอัตโนมัติเมื่อมีไฟล์ .map
Error MonitoringSentry, Datadog, Rollbar — upload .map แล้ว decode stack trace อัตโนมัติ
IDE DebuggerVS 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 ได้อัตโนมัติ
inlineembed อยู่ใน .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: trueembed .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

โครงสร้าง

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 ไฟล์ จึงเร็วและประหยัดพื้นที่


สาเหตุ

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/


วิธีที่ 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 + มี OTelnice 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