Он действительно огромный — просто посмотрите на него:
image
Эта штука весит 103кб (в сжатом виде). Больше чем код приложения — интернет-магазин -(58kb) и сравнима со всем остальным кодом в vendor бандле (156kb) — включающем react, react-dom, react-router, moment.js, lodash и кучу других библиотек. Что еще хуже — firebase нужен не на всех страницах, и очень часто не нужен к моменту загрузку сайта.


Что можно с этим сделать?


Не сликом много, как оказалось. Включение отдельных модулей не работало (на тот момент в webpack@2) да и помогло бы не слишком сильно — все равно требовалось бы включить auth и database + модуль app(42kb + 40kb + 3kb) — что дало бы 83% исходного размера так или иначе. Кроме того, сами модули auth и database совершенно монолитны (наглядно видно на скриншоте выше) и уже сжаты лучше некуда.


К слову — сжаты они с помощью Clouse Compiler или чем там Google пользуется на текущий момент — удачи минифицировать банлд с помощью uglify и не сломать ничего.

Но что-то же нужно делать!


Конечно. 103kb кода валяющиющийся мертвым грузом в бандле — это очень неприятно. Подумайте — люди жалуются на размер 'react' и переходят на inferno/preact — а react+react-dom весит всего 39kb в сжатом виде и они работают не покладая рук.


Но раз невозможно уменьшить размер этого куска — мы просто отложим его загрузку до того момента, когда он будет реально нужен.


И сделаем это так что никто и не заметит :)


Webpack и dynamic import спешит на помощь


Для сервера это предельно просто:


// firebaseImport.server.js
import * as firebase from 'firebase/firebase-node'

export default function importFirebase() {
  return Promise.resolve(firebase)
}

Для клиента — чуть сложнее, но все равно просто:


// firebaseImport.browser.js
export default function importFirebase() {
  // динамический import, вернет Promise
  // "магические" комментарии в импорте дадут возможность получить вменяемое имя
 // а не `0.js`
  return import(/* webpackChunkName: 'firebase' */
  /* webpackMode: 'lazy' */
  'firebase/firebase-browser')
}

Это все необходимо для универсального рендеринга на сервере. Если ваше приложение работает только на клиене — просто игнорируйте серверную часть

Далее нужно сделать полиморфный импорт в webpack-конфиге:


// browser webpack-config
resolve: {
  alias: {
     'firebaseImport$': path.join('path', 'to', 'your', 'firebaseImport.browser.js')
  }
}
// ...

// server webpack-config
resolve: {
  alias: {
     'firebaseImport$': path.join('path', 'to', 'your', 'firebaseImport.server.js')
  }
}
// ...

Да, мы собираем сервер webpack-ом. Постарайтесь нас не осуждать, нам нужны работающие importы в универсальном коде, а node.js не понимает их нативно.

Теперь require(firebaseImport$) вернет Promise который сразу выполнен на сервере и который лениво загрузит firebase на клиенте. После первой загрузки на клиенте этот импорт тоже станет 'выполненым', и последующие обращения к firebase будут уже почти мгновенными.


Далее нужно инициализировать клиент firebase:


export default function firebase() {
  return importFirebase().then((firebase) => {
    // Собственно инициализция firebase. Обычно что-то вроде этого:
    const app = firebase.initializeApp({
      apiKey: '<your-api-key>',
      authDomain: '<your-auth-domain>',
      databaseURL: '<your-database-url>',
      storageBucket: '<your-storage-bucket>',
      messagingSenderId: '<your-sender-id>'
    })

    // возвращаем реально используемые интерфейсы:
    return {
      database: app.database()
      auth: app.auth()
    }
  })
}

И собственно всё. Конечно, теперь использование firebase стало более многословным:


// до:
import firebase from 'what/ever/firebase'

const {auth} = firebase

export function signIn({email, password}) {
  auth.signInWithEmailAndPassword(email, password)
    .then((user) => {
      // ...
    })
}
// ---

// после:
import firebase from 'what/ever/firebase'

export function signIn({email, password}) {
  firebase().then(({auth}) => {
    auth.signInWithEmailAndPassword(email, password)
      .then((user) => {
        // ...
      })
  })
}

Но результат того стоит:
image
image


Несколько дополнительных моментов


  • Нужно не забыть добавить обработку ошибок через catch() ко всем промисам (ну и зарепортить их);
  • firebase.js можно получить на клиенте к моменту загрузки основных бандлов в большинстве браузеров — простым добавлением <link rel="preload" href="/assets/firebase.js" as="script"> в head;
  • Инициализация firebase может поругаться с hot-loader webpack'a, нам помгло использование default приложения а не именованного (https://firebase.google.com/docs/web/setup);
  • Крайне полезно время от времени запускать webpack-bundle-analyzer — размер некоторых пакетов может вас неприятно удивить (momen.js и его локали могут даже шокировать);
  • Пост находится в react.js хабе потому что говорю webpack-подразумеваю react, но (очевидно) данный метод напрямую ни от реакта ни от firebase не зависит, может быть использован для любых библиотек которые нужны "лениво";

Комментарии (21)


  1. k12th
    08.12.2017 17:42

    Постарайтесь нас не осуждать, нам нужны работающие importы в универсальном коде

    Да, но зачем вебпак-то? babel-node же.


    говорю webpack-подразумеваю react

    Почему? и то и другое никак друг на друга не завязаны.


    1. romanonthego Автор
      08.12.2017 19:58

      на самом деле все проще — так удобнее.
      Нужно собирать пререндер-entry, который использует babel с кастомным конфигом (static вот это все), нужно пересобирать его на лету для горячей перезагрузки сервера и просто для единообразия.
      кроме того собраный бандл уже независим от babel-node и прочего и запускается просто `node build/server.js`


    1. romanonthego Автор
      08.12.2017 20:07

      что касается webpack — инфраструктуры не обязаны быть связаны разумеется, но видимо уже въелось в подкорку :)


  1. Iskin
    08.12.2017 18:05

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


    https://github.com/ai/size-limit


    1. romanonthego Автор
      08.12.2017 20:03

      да, но как ответила команда firebase:

      The size issue is something we want to address as it is a known barrier to entry for those looking to use Firebase.

      Возможно ситуация изменится в последующих релизах, но пока вот так.


  1. animhotep
    08.12.2017 19:58

    в заголовке буква е пропущена firbase.js


    1. romanonthego Автор
      08.12.2017 20:03

      исправлено, мои извинения


      1. hdfan2
        09.12.2017 06:53

        И ещё в тексте «клиене».


  1. bro-dev
    08.12.2017 21:34

    В статье нету пункта где говорится почему 100кб это плохо?


    1. evocatus
      09.12.2017 00:22

      Потому что сайт бывает нужно загрузить за те секунды, что телефон словит 2G между станциями метро. Потому что экономия этих килобайт может сэкономить кругленькую сумму на хостинг или позволить держать не один сайт на одном сервере. Потому что чем быстрее загружается страница, тем позитивнее опыт её использования у клиентов.

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


    1. nsinreal
      09.12.2017 02:14

      На 100-мегабитном интернете при втором заходе на сайт (при грамотном кешировании) даже 2мб — это не плохо.
      Но вот для тех, кто впервые заходит на сайт — это плохо.
      Для тех, кто заходит на сайт через 2G/3G — это плохо.
      Для тех, у кого слабый CPU время парсинга становится убер-высоким.

      Если ваши зависимости весят по 100кб, то с развитием проекта у вас вылезет больше одной такой зависимости, что приведет к раздуванию бандла до неприличных размеров


  1. vintage
    08.12.2017 22:11

    У firebase разве нет рест апи, которое можно использовать напрямую, а не через говнолибу?


    1. romanonthego Автор
      09.12.2017 08:18

      либа не говно, просто большая. rest есть но не realtime и менее удобный.


      1. vintage
        09.12.2017 08:51

        А чего кроме говна могли туда навалить так много? :-) На поднятие вебсокет-соединения и приём-передачу сообщений много кода не надо.


  1. superconductor
    09.12.2017 06:37

    А в чем смысл убирать firebase из бандл И добавлять preload link на него? Он же таким образом будет касаться параллельно и замедлит загрузку бандла, нет?


    1. romanonthego Автор
      09.12.2017 06:42

      ну вот именно потому что параллельно — в результате получится быстрее (https://github.com/webpack/webpack/issues/3216 — тут подробнее).
      Но это именно оптимизация — preload/prefetch имеет более низкий приоритет чем собственно скрипты, и вставлять вы можете его предполагая с какой вероятностью пользователю понадобится firebase на данной странице.


  1. TiesP
    09.12.2017 09:29

    Хочется разобраться в одном вопросе. Вы написали, что само приложение (я так понял на react) занимает всего 58Кб. Я новичок в react — на одних стандартных курсах был делался проект и в итоге получался bundle.js размеров в 2Мб (хотя функционал был на порядок проще, чем интернет-магазин… конечно, никакой оптимизации не было, просто сборка) Так вот вопрос в том — в каком направлении вообще смотреть? За счет чего получается такой небольшой размер вашего приложения?


    1. TiesP
      09.12.2017 09:33

      … уточню, что использовались библиотеки babel, сборка webpack. Получался один файл, который, как я понял, включал все библиотеки — react и т.д. Но всё равно — разница довольно ощутимая получается, даже, если сложить размеры библиотек и размер вашего приложения.


      1. vintage
        09.12.2017 09:51

        58Кб — это, очевидно, после минификации и gzip сжатия.


    1. faiwer
      09.12.2017 10:32

      Если собирать вот так: NODE_ENV=production ./node_modules/.bin/webpack -p + webpack.DefinePlugin, то бандл будет радикально меньше. Будут вырезаны различные отладочные коды, плюс будет uglify. Затем сжимаем gzip (превентивно или оставляем на усмотрение nginx & пр.). И получается, что если не перегружать зависимостями то < 100 KiB. Если проект средних размеров, то < 300 KiB. 2 MiB это ну очень много.


    1. romanonthego Автор
      10.12.2017 17:45

      ох, тут по порядку:
      1) бандла 2 — vendor.js и app.js; vendor.js включает в себя почти все из node_modules, app — соответственно код самого приложения. совокупно получается около 210-220кб в сжатом виде. Плюс такого подхода — пока мы не добавили зависимость или не обновили версию зависимости в package.json — vendor.js не инвалидируется. Ну и грузится параллельно;
      2) нужно вручную следить что бы библиотки не утащили несколько версий lodash например или react-router+react-router/es или еще что-то типа того;
      3) нужно следить за размером некоторых библиотек и выносить их вот так (или любым имным способом) в ленивую загрузку или уменьшать их размер если это возможно. популярный выстрел себе в ногу — momen.js и его локали: image;
      4) uglify/gzip — разумеется; babili и иные минификаторы работают менее стабильно и дают эффект хоть и лучший — но не настолько лучший на моей практике что бы заморачиваться. пока опыт с ними отрицательный.