Source maps

При разработке фронтенд-приложений код почти всегда проходит через этапы трансформации:

  • минификация;
  • транспиляция;
  • объединение модулей;
  • tree shaking;
  • оптимизация импортов;
  • компиляция TypeScript;
  • преобразование SCSS/LESS;
  • Babel-трансформации.

После сборки исходный код перестаёт быть похожим на оригинал. В production вместо множества файлов появляются компактные bundle-файлы:

(()=>{"use strict";const e=document.querySelector("#app");e.innerHTML="Hello"})();

Без source maps отладка такого кода становится крайне сложной:

  • невозможно понять реальное место ошибки;
  • стек вызовов указывает на bundle;
  • breakpoint ставится в скомпилированный код;
  • имена переменных сокращены;
  • строки не совпадают с исходниками.

Source maps решают эту проблему. Они создают соответствие между:

  • оригинальными файлами;
  • результирующим bundle;
  • строками и колонками кода.

Браузер использует source map для восстановления исходной структуры проекта во время отладки.


Как работает source map

Source map представляет собой JSON-файл со служебной информацией:

{
  "version": 3,
  "file": "app.min.js",
  "sources": ["src/app.js"],
  "names": ["document", "querySelector"],
  "mappings": "AAAA..."
}

Основные поля:

Поле Назначение
version версия формата
file итоговый bundle
sources список исходных файлов
names имена символов
mappings таблица соответствий

В конце bundle обычно добавляется комментарий:

//# sourceMappingURL=app.js.map

Браузер загружает .map файл и связывает минифицированный код с оригинальными исходниками.


Source maps и Tom Select

Библиотека Tom Sel ect активно используется в production-сборках:

import TomSelect fr om 'tom-select';

new TomSelect('#tags');

В процессе сборки:

  • модули объединяются;
  • код минифицируется;
  • плагины оптимизируются;
  • CSS объединяется;
  • TypeScript преобразуется в JavaScript.

Без source maps ошибки внутри Tom Sel ect и пользовательских расширений становятся трудночитаемыми.

Например:

Uncaught TypeError at app.min.js:1:82931

С source maps браузер покажет:

plugins/remove_button/plugin.js:52

или:

src/components/sel ect.js:141

Генерация source maps в Vite

Vite поддерживает source maps через параметр build.sourcemap.

Базовая конфигурация

import { defineConfig } fr om 'vite';

export default defineConfig({
  build: {
    sourcemap: true
  }
});

После сборки:

dist/
 ├── assets/
 │    ├── index.js
 │    ├── index.js.map
 │    ├── index.css
 │    └── index.css.map

Режимы sourcemap в Vite

true

Полноценные отдельные .map файлы.

build: {
  sourcemap: true
}

Преимущества:

  • хорошая отладка;
  • корректные stack traces;
  • отдельные map-файлы.

Недостатки:

  • увеличение размера сборки;
  • дополнительный HTTP-запрос.

inline

Source map встраивается внутрь bundle.

build: {
  sourcemap: 'inline'
}

Результат:

//# sourceMappingURL=dat a:application/json;base64,...

Преимущества:

  • не нужен отдельный .map;
  • удобно для локальной разработки.

Недостатки:

  • сильно увеличивается размер JS.

hidden

Map создаётся, но ссылка в bundle отсутствует.

build: {
  sourcemap: 'hidden'
}

Используется для:

  • Sentry;
  • Rollbar;
  • серверной диагностики;
  • production-мониторинга.

Браузер не увидит source map автоматически.


Source maps в Webpack

Webpack предоставляет более гибкую систему.

devtool

Главный параметр:

module.exports = {
  devtool: 'source-map'
};

Основные режимы Webpack

source-map

Максимально точные source maps.

devtool: 'source-map'

Подходит для production.


inline-source-map

Source map внутри bundle.

devtool: 'inline-source-map'

Подходит для разработки.


eval-source-map

Очень быстрый режим.

devtool: 'eval-source-map'

Используется dev-сервером.


cheap-source-map

Упрощённые карты.

devtool: 'cheap-source-map'

Особенности:

  • без column mappings;
  • быстрее сборка;
  • хуже точность.

hidden-source-map

Скрытые production-карты.

devtool: 'hidden-source-map'

Часто применяется вместе с Sentry.


Source maps в Rollup

Tom Sel ect часто используется вместе с Rollup.

Включение

export default {
  output: {
    sourcemap: true
  }
};

Source maps в esbuild

esbuild поддерживает несколько режимов.

external

await esbuild.build({
  sourcemap: true
});

Создаётся отдельный .map.


inline

await esbuild.build({
  sourcemap: 'inline'
});

both

await esbuild.build({
  sourcemap: 'both'
});

Создаются:

  • встроенная карта;
  • отдельный .map.

Source maps в TypeScript

TypeScript умеет генерировать карты исходников самостоятельно.

tsconfig.json

{
  "compilerOptions": {
    "sourceMap": true
  }
}

Результат:

app.js
app.js.map

Inline sources

TypeScript может встраивать оригинальный код прямо в map.

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

Преимущество:

  • debugger видит исходник даже без доступа к файлам проекта.

Связь source maps и Babel

Babel умеет передавать source maps между этапами трансформации.

Пример

module.exports = {
  sourceMaps: true
};

При цепочке:

TypeScript → Babel → Webpack

source maps объединяются.


Многоступенчатые source maps

Современная сборка может выглядеть так:

SCSS
  ↓
PostCSS
  ↓
CSS bundle
  ↓
Minifier

или:

TypeScript
  ↓
Babel
  ↓
Webpack
  ↓
Terser

Каждый этап обязан корректно передавать source maps дальше.

Иначе:

  • debugger покажет неверные строки;
  • stack trace станет бесполезным;
  • breakpoints начнут смещаться.

Source maps и минификация

Минификаторы:

  • Terser;
  • esbuild;
  • SWC;
  • UglifyJS;

умеют сохранять mapping.

Terser

new TerserPlugin({
  extractComments: false
});

Webpack автоматически связывает source maps с Terser.


Отладка Tom Select через source maps

Ошибка плагина

Без source maps:

main.js:1

С source maps:

plugins/dropdown_input/plugin.ts:74

Отладка пользовательского render

new TomSelect('#users', {
  render: {
    option(data, escape) {
      return `<div>${data.name.toUpperCase()}</div>`;
    }
  }
});

При ошибке debugger покажет реальную строку:

data.name.toUpperCase()

а не минифицированный bundle.


Breakpoints и source maps

DevTools браузера умеет:

  • ставить breakpoint в TypeScript;
  • дебажить ES-модули;
  • отслеживать async stack traces;
  • восстанавливать оригинальные имена.

Без source maps

Breakpoint:

bundle.js:1

С source maps

Breakpoint:

src/select/renderers.ts:91

Source maps для CSS

Tom Select содержит CSS-стили:

.ts-wrapper {
    position: relative;
}

После минификации:

.ts-wrapper{position:relative}

Source maps позволяют:

  • находить оригинальный SCSS;
  • видеть файл источника;
  • отслеживать PostCSS-трансформации.

Генерация CSS source maps в Vite

export default defineConfig({
  css: {
    devSourcemap: true
  }
});

Source maps и PostCSS

module.exports = {
  map: true
};

PostCSS сохранит информацию о:

  • autoprefixer;
  • nesting;
  • custom properties;
  • cssnano.

Source maps и Sass

sass.render({
  file: 'style.scss',
  sourceMap: true
});

Debugger сможет открыть:

style.scss

вместо:

style.css

Проблемы безопасности

Source maps могут раскрывать:

  • структуру проекта;
  • внутренние API;
  • приватные комментарии;
  • имена файлов;
  • исходный TypeScript;
  • архитектуру приложения.

Поэтому production source maps требуют осторожности.


Типичные production-стратегии

Полное отключение

build: {
  sourcemap: false
}

Максимальная безопасность.

Минусы:

  • сложная диагностика ошибок.

Hidden source maps

build: {
  sourcemap: 'hidden'
}

Оптимальный production-вариант.


Отдельное хранение

Map-файлы:

  • не публикуются;
  • загружаются в Sentry;
  • доступны только CI/CD.

Source maps и Sentry

Sentry умеет декодировать stack trace.

Пример

Без source maps:

main.91d2.js:1:82931

С source maps:

src/plugins/tags.ts:114

Upload source maps в Sentry

Обычно используется:

sentry-cli releases files upload-sourcemaps

После загрузки Sentry автоматически восстанавливает:

  • имена файлов;
  • строки;
  • stack trace;
  • функции.

Performance-влияние

Source maps влияют на:

  • время сборки;
  • объём bundle;
  • скорость devtools.

Особенно заметно:

  • при больших SPA;
  • множестве модулей;
  • крупных dependency graphs.

Оптимизация source maps

Использование cheap maps

devtool: 'cheap-module-source-map'

Ускоряет rebuild.


Отключение column mappings

Полные column mappings тяжёлые.

Cheap-режимы используют только строки.


Разделение dev и production

const isProd = process.env.NODE_ENV === 'production';

export default {
  build: {
    sourcemap: !isProd
  }
};

Проверка source maps

Chrome DevTools

Вкладка:

Sources

Появляются:

  • TypeScript-файлы;
  • оригинальные модули;
  • структура проекта.

Проверка через Network

Необходимо убедиться в наличии:

app.js.map

Проверка sourceMappingURL

В конце bundle:

//# sourceMappingURL=app.js.map

Типичные проблемы

Source maps отсутствуют

Причины:

  • отключены в bundler;
  • удалены сервером;
  • не попали в deploy.

Неверные пути

Ошибка:

DevTools failed to load source map

Причина:

404 app.js.map

Смещённые breakpoints

Причины:

  • некорректная цепочка Babel;
  • старые maps;
  • двойная минификация;
  • конфликт loader-ов.

Повреждённые mappings

Возможны при:

  • несовместимых loader-ах;
  • ручной постобработке bundle;
  • устаревших плагинах.

Source maps и CDN

CDN может:

  • кешировать старые maps;
  • отдавать неверные версии;
  • удалять .map.

Важно синхронизировать:

bundle hash

и:

source map hash

Source maps и lazy loading

Tom Select может подключаться динамически:

const { default: TomSelect } =
    await import('tom-select');

Каждый chunk получает собственный source map:

vendor.js
vendor.js.map

Source maps и tree shaking

После tree shaking:

  • часть кода удаляется;
  • модули объединяются;
  • строки смещаются.

Корректный bundler обязан пересчитать mappings.


Source maps и async chunks

При code splitting:

main.js
admin.js
vendors.js

каждый chunk имеет собственную карту:

main.js.map
admin.js.map
vendors.js.map

Source maps и HMR

Во время Hot Module Replacement source maps:

  • обновляются динамически;
  • пересчитываются при rebuild;
  • позволяют дебажить изменённые модули без перезагрузки страницы.

Source maps в Node.js

Node.js тоже поддерживает source maps.

Флаг

node --enable-source-maps app.js

Стек ошибок TypeScript станет читаемым:

src/server.ts:41

вместо:

dist/server.js:1

Source maps и SWC

SWC поддерживает генерацию карт:

{
  "sourceMaps": true
}

Используется в:

  • Next.js;
  • Turbopack;
  • Rspack;
  • NestJS.

Source maps и Vitest

Vitest использует source maps для:

  • корректных stack traces;
  • snapshot-ошибок;
  • покрытия кода;
  • дебага тестов.

Source maps и coverage

Инструменты покрытия:

  • Istanbul;
  • c8;
  • Vitest coverage;

используют source maps для сопоставления:

coverage → original source

Иначе coverage будет рассчитан по bundle.


Практическая production-конфигурация для Tom Select

Vite

import { defineConfig } fr om 'vite';

export default defineConfig({
  build: {
    sourcemap: 'hidden',
    minify: 'esbuild'
  },
  css: {
    devSourcemap: true
  }
});

Практическая development-конфигурация

import { defineConfig } fr om 'vite';

export default defineConfig({
  build: {
    sourcemap: true
  }
});

Рекомендации по использованию

Development

Лучшие варианты:

  • source-map;
  • inline-source-map;
  • eval-source-map.

Production

Предпочтительно:

  • hidden-source-map;
  • отдельное хранение maps;
  • загрузка в monitoring-систему.

Когда source maps особенно важны

При использовании TypeScript

Транспиляция сильно меняет структуру кода.


При сложных render-функциях Tom Select

render: {
  item(data) {
    return complexTemplate(data);
  }
}

При кастомных плагинах

TomSelect.define('my_plugin', function() {
    // complex logic
});

При code splitting

Множество chunks усложняют диагностику.


При использовании Babel

Особенно с:

  • decorators;
  • class properties;
  • async transforms;
  • polyfills.

Совместимость браузеров

Source maps поддерживаются:

  • Chrome;
  • Firefox;
  • Edge;
  • Safari.

Поддержка реализована через DevTools и не влияет на выполнение JavaScript.


Формат Source Map V3

Современные bundler-ы используют спецификацию:

Source Map Revision 3

Она поддерживает:

  • column mappings;
  • multiple sources;
  • names mapping;
  • sections;
  • inline sources.

Inline sourcesContent

Map может хранить исходники внутри себя:

{
  "sourcesContent": [
    "const app = new TomSelect(...);"
  ]
}

Преимущества:

  • debugger работает без файловой системы;
  • удобнее remote debugging.

Недостатки:

  • большой размер .map.

Debugging workflow с Tom Select

Типичный процесс:

  1. Ошибка появляется в production.
  2. Stack trace указывает на bundle.
  3. Monitoring-система использует source maps.
  4. Происходит восстановление оригинального файла.
  5. Определяется точная строка ошибки.
  6. Исправление вносится в исходный TypeScript/JavaScript.

Без source maps такой workflow практически невозможен в крупных frontend-проектах.