Полифилы: автоматические и ручные подходы

Полифилы (polyfills) — это реализации возможностей JavaScript, отсутствующих в старых браузерах или окружениях выполнения. Их задача заключается в обеспечении совместимости современного кода с платформами, не поддерживающими определённые API, синтаксис или встроенные объекты.

Полифил может:

  • добавлять отсутствующие методы;
  • эмулировать новые API;
  • подключать runtime-реализации;
  • расширять глобальные объекты;
  • заменять недоступный функционал альтернативной реализацией.

Наиболее распространённые примеры:

Promise
fetch
Array.prototype.flat
Object.fromEntries
URL
Symbol
Map
Set

Webpack напрямую не создаёт полифилы, однако тесно интегрируется с инструментами их автоматического и ручного подключения.


Различие между транспиляцией и полифилами

Очень важно различать:

Инструмент Назначение
Babel Преобразование синтаксиса
Полифилы Реализация отсутствующих API

Пример:

const fn = async () => {
  await fetch('/api');
};

Babel способен преобразовать async/await в совместимый код:

function fn() {
  return regeneratorRuntime.async(function fn$() {
    // ...
  });
}

Но Babel не реализует:

fetch
Promise
URL
Map
Set

Если браузер не поддерживает эти API, требуется полифил.


Основные подходы к полифилам

Существует два базовых подхода:

Подход Описание
Автоматический Полифилы подключаются автоматически на основе кода и целевых браузеров
Ручной Разработчик самостоятельно импортирует необходимые полифилы

Browserslist как основа определения совместимости

Большинство современных инструментов используют Browserslist.

Пример:

{
  "browserslist": [
    "> 0.5%",
    "last 2 versions",
    "not dead"
  ]
}

Webpack сам по себе не анализирует совместимость API, но:

  • Babel;
  • core-js;
  • Autoprefixer;
  • PostCSS;
  • SWC;

используют Browserslist для определения необходимых преобразований.


Core-js

core-js — крупнейшая библиотека полифилов для JavaScript.

Поддерживает:

  • ECMAScript API;
  • Stage proposals;
  • web standards;
  • iterator helpers;
  • structured cloning;
  • typed arrays;
  • Promise utilities.

Установка:

npm install core-js

Для Babel:

npm install core-js regenerator-runtime

Автоматические полифилы через Babel

Preset-env

Основной механизм автоматического подключения:

npm install @babel/preset-env babel-loader --save-dev

Конфигурация:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              [
                '@babel/preset-env',
                {
                  useBuiltIns: 'usage',
                  corejs: 3
                }
              ]
            ]
          }
        }
      }
    ]
  }
};

Режим useBuiltIns

Существует три режима.

false

Полифилы не подключаются автоматически.

{
  useBuiltIns: false
}

Только синтаксические преобразования.


entry

Babel анализирует импортированный общий полифил и заменяет его набором необходимых модулей.

Точка входа:

import 'core-js';
import 'regenerator-runtime/runtime';

Babel преобразует это в набор конкретных импортов.


usage

Наиболее популярный режим.

Babel анализирует используемые API и подключает только нужные полифилы.

Исходный код:

const arr = [1, 2, 3];

arr.flat();

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

import "core-js/modules/es.array.flat.js";
import "core-js/modules/es.array.unscopables.flat.js";

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

  • минимальный размер bundle;
  • автоматическое подключение;
  • отсутствие ручных импортов.

Недостатки:

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

Автоматическое подключение regenerator-runtime

Для:

async/await
generators

необходим runtime:

npm install regenerator-runtime

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

useBuiltIns: 'usage'

Babel подключает runtime автоматически.


Пример полной конфигурации Babel + Webpack

const path = require('path');

module.exports = {
  mode: 'production',

  entry: './src/index.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,

        use: {
          loader: 'babel-loader',

          options: {
            presets: [
              [
                '@babel/preset-env',
                {
                  targets: '> 0.5%, not dead',
                  useBuiltIns: 'usage',
                  corejs: 3
                }
              ]
            ]
          }
        }
      }
    ]
  }
};

Как Babel определяет необходимые полифилы

Babel:

  1. Анализирует AST;
  2. Проверяет используемые API;
  3. Сопоставляет их с Browserslist;
  4. Подключает нужные модули core-js.

Например:

Promise.any()

может привести к подключению:

core-js/modules/es.promise.any.js

Автоматические полифилы и tree shaking

core-js разбит на модули.

Это позволяет:

  • подключать только используемые возможности;
  • уменьшать bundle;
  • эффективно использовать tree shaking.

Однако глобальные полифилы часто считаются side effects, поэтому полностью удалить их невозможно.


Ручное подключение полифилов

Во многих проектах предпочтителен ручной контроль.

Прямой импорт

import 'core-js/features/promise';
import 'core-js/features/array/flat';

Импорт отдельных модулей

import 'core-js/modules/es.promise';
import 'core-js/modules/es.array.flat';

Полный импорт

import 'core-js/stable';

Подключает практически весь ECMAScript polyfill layer.

Недостатки:

  • огромный bundle;
  • ненужные API;
  • ухудшение производительности загрузки.

Runtime-only подход

Некоторые проекты избегают глобального загрязнения среды.

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

npm install @babel/plugin-transform-runtime
npm install @babel/runtime

Конфигурация:

{
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3
      }
    ]
  ]
}

Отличие transform-runtime от useBuiltIns

useBuiltIns

Добавляет полифилы в глобальную область:

Promise
Array.prototype.flat

становятся глобально доступными.


transform-runtime

Не загрязняет глобальную область.

Babel импортирует helper-функции локально:

import _Promise from "@babel/runtime-corejs3/core-js/promise";

Подходит для:

  • библиотек;
  • SDK;
  • npm-пакетов;
  • shared-модулей.

Полифилы для библиотек

Библиотеки обычно не должны:

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

Поэтому библиотекам рекомендуется:

@babel/plugin-transform-runtime

вместо:

useBuiltIns

Полифилы и Webpack 5

Webpack 4 автоматически добавлял polyfills для Node.js-модулей:

Buffer
process
crypto
stream
path

Webpack 5 отказался от автоматических полифилов.

Теперь требуется ручная настройка.


Node.js polyfills в Webpack 5

Пример ошибки:

Module not found: Error: Can't resolve 'crypto'

Решение:

npm install crypto-browserify

resolve.fallback

module.exports = {
  resolve: {
    fallback: {
      crypto: require.resolve('crypto-browserify'),
      stream: require.resolve('stream-browserify'),
      buffer: require.resolve('buffer')
    }
  }
};

ProvidePlugin для глобальных объектов

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      Buffer: ['buffer', 'Buffer'],
      process: 'process/browser'
    })
  ]
};

Почему Webpack отказался от автоматических polyfills

Причины:

  • скрытое увеличение bundle;
  • плохой контроль зависимостей;
  • устаревшие browser polyfills;
  • неожиданное поведение;
  • сложность поддержки.

Conditional polyfills

Иногда полифил подключается только при необходимости.

Пример:

if (!window.Promise) {
  await import('core-js/features/promise');
}

Dynamic import и lazy polyfills

Можно загружать полифилы динамически:

async function loadPolyfills() {
  if (!('IntersectionObserver' in window)) {
    await import('intersection-observer');
  }
}

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

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

Polyfill.io

Сервис автоматической выдачи полифилов:

<script src="https://polyfill.io/v3/polyfill.min.js"></script>

Сервис определяет браузер и отправляет только необходимые полифилы.


Недостатки CDN polyfills

Проблемы:

  • внешняя зависимость;
  • сетевые задержки;
  • вопросы безопасности;
  • невозможность полного контроля;
  • проблемы CSP.

Полифилы DOM API

Babel не полифилит DOM API.

Например:

IntersectionObserver
ResizeObserver
fetch
AbortController

требуют отдельных библиотек.


Fetch polyfill

Популярный вариант:

npm install whatwg-fetch

Импорт:

import 'whatwg-fetch';

AbortController

npm install abortcontroller-polyfill

IntersectionObserver

npm install intersection-observer

URL и URLSearchParams

Некоторые старые браузеры не поддерживают:

URL
URLSearchParams

Полифил:

npm install core-js

или специализированные библиотеки.


Promise polyfills

Старые браузеры:

  • Internet Explorer;
  • ранние Android WebView;

не поддерживают Promise.

Полифил:

import 'core-js/features/promise';

Symbol polyfills

Symbol крайне сложен для полной эмуляции.

Некоторые возможности невозможно воспроизвести полностью:

  • настоящая приватность ключей;
  • внутренние спецификационные механизмы.

Полифилы и производительность

Проблемы чрезмерного количества полифилов:

  • увеличение bundle;
  • рост parse time;
  • увеличение memory footprint;
  • более медленный startup;
  • ухудшение Time To Interactive.

Оптимизация стратегии полифилов

Эффективные подходы:

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

useBuiltIns: 'usage'

Актуальный Browserslist

Избыточная поддержка старых браузеров резко увеличивает размер bundle.


Разделение modern/legacy сборок

Современный подход:

<script type="module" src="modern.js"></script>
<script nomodule src="legacy.js"></script>

Differential serving

Можно собирать:

Bundle Назначение
modern Современные браузеры
legacy Старые браузеры

Современные браузеры получают минимальный bundle без лишних полифилов.


Debug режим preset-env

Позволяет увидеть подключаемые полифилы.

{
  debug: true
}

Пример вывода:

Using polyfills:
  es.array.flat
  es.promise

Core-js versions

Крайне важно явно указывать версию:

corejs: 3

Без указания версии Babel может работать некорректно.


Опасность глобальных полифилов

Некоторые полифилы:

  • изменяют прототипы;
  • конфликтуют с библиотеками;
  • ломают проверки instanceof;
  • изменяют поведение итераторов.

Особенно опасны:

Array.prototype
Object.prototype
String.prototype

Side effects полифилов

Полифилы почти всегда имеют side effects.

Например:

Array.prototype.flat = function () {}

Это глобальное изменение среды выполнения.


Проверка поддержки возможностей

Часто используется feature detection:

if (!Array.prototype.flat) {
  // polyfill
}

или:

if (!window.fetch) {
  // polyfill
}

Полифилы и TypeScript

TypeScript не добавляет полифилы.

Даже если код успешно компилируется:

Promise.any()

это не означает наличие поддержки в браузере.


tslib и полифилы

tslib содержит helper-функции TypeScript, но не полифилы ECMAScript API.


SWC и полифилы

SWC поддерживает:

  • транспиляцию;
  • target environments.

Но экосистема автоматических полифилов у SWC менее зрелая по сравнению с Babel.

Часто используется комбинированный подход:

SWC + core-js

Esbuild и полифилы

Esbuild не занимается автоматическим полифиллингом API.

Он:

  • преобразует синтаксис;
  • меняет target;
  • оптимизирует код.

Но не добавляет:

Promise
fetch
Map
Set

Legacy browser support

Наиболее проблемные браузеры:

Браузер Особенности
Internet Explorer 11 отсутствие Promise, fetch, Map
Старые Safari частичная поддержка ES features
Android 4 WebView множество ограничений
Старые Samsung Internet неполная реализация API

Проверка итоговой совместимости

Используются инструменты:

npx browserslist

и:

npx core-js-compat

Анализ bundle

Полифилы могут занимать значительную часть сборки.

Полезные инструменты:

webpack-bundle-analyzer
source-map-explorer

Типичная современная конфигурация

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: '>0.25%, not dead',

        useBuiltIns: 'usage',

        corejs: 3,

        bugfixes: true
      }
    ]
  ]
};

Когда использовать автоматические полифилы

Подход хорошо подходит для:

  • SPA;
  • корпоративных приложений;
  • frontend-сборок;
  • проектов с широкой browser support matrix.

Когда предпочтителен ручной подход

Ручной контроль часто выбирается для:

  • библиотек;
  • SDK;
  • performance-critical приложений;
  • микрофронтендов;
  • embedded environments.

Комбинированный подход

Во многих крупных проектах используется смешанная стратегия:

  • preset-env для ECMAScript;
  • ручные DOM polyfills;
  • dynamic imports;
  • differential serving;
  • selective runtime transforms.

Такой подход обеспечивает:

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