Мы все любим ES6, и с Babel мы можем использовать новейший синтаксис в браузерах, которые его еще не поддерживают. Однако в некоторых браузерах отсутствует поддержка не только новейшего синтаксиса, но и некоторых новых функций, которые Babel не может добавить простым преобразованием кода.

Хорошим примером такой функции является асинхронный fetch, который недоступен, например, в IE 11.

В таких ситуациях нам нужно импортировать библиотеку, которая добавит недостающую функцию только при запуске в браузере, не поддерживающем ее. Этот распространенный прием называется полифиллинг.

Для fetch есть отличный полифилл под названием whatwg-fetch, поэтому в таком случае мы обычно устанавливаем пакет:

npm i --save whatwg-fetch

И затем укажите на него ссылку во входном файле (index.js), чтобы убедиться, что он выполняется непосредственно перед загрузкой остальной части приложения:

import 'whatwg-fetch'

Таким образом, нам не нужно ссылаться на него в каком-либо другом модуле, а сам пакет ничего не делает, если браузер уже поддерживает fetch изначально.

Представление

Как это влияет на производительность вашего приложения? Пакет whatwg-fetch имеет размер около 8 КБ (3 КБ в сжатом виде), поэтому, скорее всего, он не окажет большого влияния, но по-прежнему должен быть загружен и обработан всеми браузерами излишне, что кажется немного бесполезным.

К счастью, предложение ES6 динамический импорт (читайте также здесь) вместе с фрагментированием Webpack очень удобно и позволяет нам лениво загружать такие зависимости только тогда, когда они действительно необходимы.

Другими словами, динамический импорт вместо ссылки на модуль возвращает Promise, который выполняется после полной загрузки модуля:

import('module/path/file.js')
  .then(someModule => someModule.foo())
  .catch((e) => console.error(e))

Это позволяет нам дождаться выполнения остальной части приложения, пока модуль с отложенной загрузкой не станет полностью доступным.

Ленивая загрузка полифиллов

Предполагая, что ваше приложение уже использует Webpack и Babel, первое, что вам нужно сделать, это включить поддержку динамического импорта в babel:

npm i --save-dev @babel/plugin-syntax-dynamic-import

А затем добавьте его в babel.config.js

plugins: [
  '@babel/plugin-syntax-dynamic-import'
]

Теперь мы можем изменять полифил index.js и ленивую загрузку fetch только тогда, когда это необходимо:

if (!window.fetch) {
  fetch.push(import(/* webpackChunkName: "polyfill-fetch" */ 'whatwg-fetch'))
}

Webpack достаточно умен, чтобы знать, что при анализе динамического импорта нам не нужен этот файл немедленно. Таким образом, он автоматически переместит его в отдельный кусок. После запуска сборки получим:

app.js
polyfill-fetch.js

Имя этого дополнительного файла определяется с помощью директивы webpackChunkName, и мы можем объединить множество полифиллов в один кусок, если захотим. Милая!

Подождите, пока загрузится требуемый полифилл

Однако мы не сможем продолжить работу с приложением, пока такой полифилл не будет загружен. Предположим, что наш index.js понравился:

import 'whatwg-fetch'
import app from './app.js'
app()

Теперь, когда мы загружаем fetch лениво, подождем, пока он не станет доступен:

import app from './app.js'
const polyfills = []
if (!window.fetch) {
  polyfills.push(import(/* webpackChunkName: "polyfill-fetch" */ 'whatwg-fetch'))
}
Promise.all(polyfills)
  .then(app)
  .catch((error) => {
    console.error('Failed fetching polyfills', error)
  })

Если polyfillsarray пуст, app будет выполняться немедленно. В противном случае, если в браузерах отсутствует fetch, он будет ждать загрузки.

Ждите кучу полифиллов

Если вам нужно дождаться кучи полифилов, вы можете обработать логику для их загрузки в отдельные файлы:

// polyfills/a.js
const polyfillA = []
if (condition) {
  polyfillA.push(import(/* webpackChunkName: "polyfill-a" */ 'a..'))
}
export default polyfillA
// polyfills/b.js
const polyfillB = []
if (condition) {
  polyfillB.push(import(/* webpackChunkName: "polyfill-b" */ 'b..'))
  polyfillB.push(import(/* webpackChunkName: "polyfill-b" */ 'c..'))
}
export default polyfillB

Которую затем можно объединить в одноpolyfills/index.js

import polyfillA from './a'
import polyfillB from './b'
export default [
  ...polyfillA,
  ...polyfillB
]

И загружается параллельно в index.js

import polyfills from './polyfills'
import app from './app.js'
Promise.all(polyfills)
  .then(app)
  .catch((error) => {
    console.error('Failed fetching polyfills', error)
  })

Альтернативные решения

Одним из наиболее популярных альтернативных решений является сервис polyfill.io, использующий определение функций на стороне сервера на основе идентификации User-Agent. Хотя также можно использовать обнаружение функций на стороне клиента аналогично представленному в этой статье.

Что делает представленное решение лучше:

  • полностью контролируемое обнаружение функций на стороне клиента
  • полностью контролируемый выбор библиотеки полифиллов (и версии)
  • Webpack treehaking
  • использовать исключительно собственный домен (в случае, если polyfill.io будет заблокирован вашим клиентским брандмауэром)

Заключение

Разделение полифиллов на отдельные части помогает ограничить размер приложения для большинства пользователей, использующих современный браузер. Тем не менее, они остаются доступными для старых.

Параллельная отложенная загрузка полифилов ограничивает их негативное влияние, особенно для HTTP1.0, однако имейте в виду, что старые браузеры обычно загружали около 4 файлов javascript одновременно.

Взгляните на демонстрационный проект, реализующий описанное решение.

Благодарим Мэтта Буна за корректуру, прочитанную в этой статье.