company_banner

Хватит импортировать JavaScript-пакеты целиком

Автор оригинала: Aakash Yadav
  • Перевод
Часто ли вам доводилось импортировать в проект целый JavaScript-пакет, когда нужно было воспользоваться лишь очень немногими возможностями этого пакета? Вероятно, вы сможете вспомнить много таких случаев.

Ярким примером пакета, который импортируют целиком, нуждаясь лишь в нескольких его функциях, можно назвать библиотеку Lodash. Если вы не слышали об этой библиотеке — вам стоит взглянуть на её документацию. В частности, на сайте проекта можно узнать о том, что Lodash упрощает JavaScript-разработку, беря на себя решение задач по работе с массивами, числами, объектами, и прочим подобным.



Библиотека Lodash включает в себя более 200 функций. Это говорит о том, что она, и правда, способна помочь программисту в решении массы задач. Но может случиться так, что, импортировав всю библиотеку, вызовут лишь 4-5 функций. Это приводит нас к вопросу о целесообразности импорта всего пакета в ситуации, когда использовано будет лишь 2-3% его возможностей. Подумаем о том, как с этим справиться.

Функция get из библиотеки Lodash


Одна из моих любимых возможностей библиотеки Lodash представлена функцией get. Она позволяет организовать безопасный доступ к вложенным объектам и поддерживает применение значений, задаваемых по умолчанию.

Вот пример использования этой функции:

const _ = require('lodash');

let employee1 = {
  name : "Jon",
  address : {
    street : "North Avenue",
    area : "DAC",
    zip : "87344",
    contact : [ 12444554, 9384847 ]
  },
  designation : "Manager"
};

let employee2 = {
  name : "Jake",
  designation : "Senior Manager"
};

function getHomeContact(employee) {
  return employee.address.contact;
}

getHomeContact(employee1); // [12444554, 9384847]
getHomeContact(employee2); // Uncaught TypeError: Cannot read property 'contact' of undefined

function getHomeContactWithLodash(employee) {
  return  _.get(employee, "address.contact", []);
}

getHomeContactWithLodash(employee1); // [12444554, 9384847]
getHomeContactWithLodash(employee2); // []

Использование этой функции позволяет сделать код гораздо чище, чем прежде. Это помогает избежать ошибок из-за того, что, когда ожидается пустой массив, функция не вернёт null. Благодаря этой функции нельзя, по ошибке, вызвать метод map у пустого массива. Она защищает и от других неприятностей.

Взглянем на то, как повлияет на размер бандла включение в проект библиотеки Lodash в том случае, если планируется использовать лишь функцию get. Эксперименты будут проводиться с использованием React-проекта. Размер бандла будет проанализирован до импорта библиотеки и после различных вариантов подключения её к проекту.

Размер проекта до импорта библиотеки


Проанализируем размер файлов проекта до импорта библиотеки.


Размер файлов до использования Lodash

Теперь посмотрим на последствия нескольких способов импорта библиотеки в проект.

Размер проекта после использования разных способов импорта библиотеки


▍1. Традиционный импорт


Речь идёт об импорте библиотеки одним из следующих традиционных способов.

Первый:

import _ from ‘lodash’;

Второй:

const _ = require('lodash');

Вот как это повлияет на итоговый размер файлов проекта.


Изменение размеров файлов проекта при импорте всего пакета

▍2. ES6-импорт


Здесь у нас, опять же, есть два варианта.

Первый:

import { get } from 'lodash';

Второй:

const { get } = require('lodash');

Взглянем на влияние такого импорта на размер файлов проекта.


Размеры файлов при импорте функции get с использованием деструктурирующего присваивания

Как видите, применение обоих вышеописанных подходов привело к увеличению размеров файлов примерно на 23 Кб. А это — весьма значительная прибавка, особенно учитывая то, что речь идёт об использовании единственной функции из библиотеки, в которую входит более 200 функций. В итоге оказывается, что размер бандла увеличился настолько, насколько можно было бы ожидать его увеличения в том случае, если бы в проекте использовались бы все эти 200 функций.

Может быть, 23 Кб — это не такая уж и большая цена за использование единственной нужной функции? Нет, это — слишком много. 

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

Проанализируем папку, в которой хранятся материалы Lodash.

Для этого достаточно перейти по пути node_modules/lodash. В этой папке можно найти множество файлов, в которых хранится код отдельных функций. Среди них несложно найти файл get.js, в котором находится код интересующей нас функции get. А это значит, что если нам нужна только функция get — достаточно импортировать в проект лишь этот файл. Это ведёт нас к третьему способу импорта.

▍3. Импорт файла get.js из Lodash


Тут, снова, доступны два способа.

Первый:

import get from 'lodash/get';

Второй:

const get = require('lodash/get');

Взглянем на изменение размеров бандла.


Размеры файлов при импорте файла get.js

Видно, что благодаря тому, что мы импортировали в проект только файл get.js, мы смогли избавиться от более чем 20 Кб ненужного кода, попадающего в бандл при использовании других методов. А ведь речь идёт лишь об одном пакете. В типичном JavaScript-проекте гораздо больше зависимостей. Представьте себе то, как осторожный подход к импорту пакетов и постоянный контроль размеров бандла могут повлиять на некий серверный или клиентский проект.

Все ли пакеты поддерживают выборочный подход к импорту?


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

Как сделать то же самое, работая с другими библиотеками?


Процесс удаления неиспользуемого кода известен как «встряска дерева» (tree shaking). Если нужно, например, «встряхнуть дерево» библиотеки Ant Design — поищите в интернете по словам «antd tree shaking». Вы вполне можете найти обсуждение этого вопроса на StackOverflow или на GitHub. Найдя такое обсуждение — просмотрите его — вполне возможно, кто-то уже решил стоящую перед вами задачу.

Ещё один способ избавления от ненужного кода требует приложить немного больше усилий. Нужно зайти в папку пакета, находящуюся в node_modules, и заняться анализом кода. В частности, следует поинтересоваться структурой проекта и узнать, разбит ли он на небольшие модули, которые можно импортировать в проект независимо друг от друга.

Пример оптимизации импорта при использовании пакета antd


До:

import { Menu} from 'antd';

После:

import Menu from 'antd/es/menu';
import 'antd/es/menu/style/css';

Пример оптимизации импорта при использовании пакета material-ui


До:

import { Button } from '@material-ui/core';

После:

import Button from '@material-ui/core/Button';

Пример оптимизации импорта при использовании пакета moment


Оптимизации импорта библиотеки moment выглядит немного сложнее. Поэтому, если вам это нужно, взгляните на задачу, которую я создал в трекере задач этой библиотеки.

Итоги


Вы сами можете убедиться в том, что в ваши проекты попадает немало ненужного кода. Решить эту проблему можно, внимательно сопоставляя то, что именно вы импортируете, с тем, что именно используете. Это позволит вам, например, сократить время загрузки ваших сайтов. Поэтому рекомендуется внимательно следить за размерами бандлов проектов и оптимизировать импорт зависимостей.

А как вы, в своих JavaScript-проектах, боретесь с импортом ненужного кода?

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Комментарии 36

    +5

    Просто оставлю это здесь (babel-plugin-import). Не пишите import X from 'lib/X', ей богу. Зачем вам этот boilerplate.

      +1

      А еще я бы добавил вот этот репозиторий из серии you-dont-need-%library%.


      Иногда вместо танцев с бубном стоит этот бубен не использовать.


      Практически весь lodash заменим нативным Javascript функционалом.
      Moment.js можно заменить альтернативной, браузер-ориентированной библиотекой.
      А Material-UI стал поддерживать tree-shaking из-коробки.

        +6

        Иногда действительно есть адекватные замены в несколько ПОНЯТНЫХ строк, а иногда:


        // Lodash
        var grouped = _.groupBy(['one', 'two', 'three'], 'length')
        
        // Native
        var grouped = ['one', 'two', 'three'].reduce((r, v, i, a, k = v.length) => ((r[k] || (r[k] = [])).push(v), r), {})

        Я бы на код ревью эту кашу из магических r,v,i,a,k не пропустил

          +1

          Ключевое слово здесь "иногда". А если бездумно тащить каждую популярную библиотеку на initial load, никакие метрики скорости не пройдут. Конечно, если писать под electron, то можно и Moment.js тянуть без tree-shaking.


          А придумывать в качестве аргумента задачи защищающие lodash — довольно странно.

            +2

            Пусть вы начали писать новый проект, понадобился map по объектам, но вы не взяли лодаш, вы написали Object.values(arrayLikeObject).map


            Через несколько дней вам понадобился последний элемент массива и вы написали numbers[numbers.length — 1], зачем тут лодаш?


            Еще через несколько дней вам понадобился debounce, вы либо написали свой, либо поставили другую(не лодаш) библиотеку где есть debounce


            Еще через несколько дней вам понадобился initial, но вы написали array.slice (и десять раз подумали не перепутали ли вы splice и slice)


            Еще через пару дней вам понадобилось отфильтровать что-то из массива, и вы написали array.filter(function(value) {
            return value !== filteredValue;
            }) и потеряли скорость выполнения кода, ибо вам пришлось заюзать фильтр чтоб получить иммутабельность там где лодаш дает и мутабельные и иммутабельные варианты типа without, pull и так далее


            Еще через какое-то время вам понадобился этот ужасный groupBy который нативным методов выглядит жутко. И воооот тут вы ставите лодаш.


            А теперь вопрос, через сколько дней (или строк линий кода) в среднем наступит этот момент что лодаш уже можно ставить, и сколько человеко-часов вы потеряли в поисках как нативно сделать простые вещи типа Object.values(obj).map ?


            Сколько человеко-QA-часов вы потеряете из-за того что перепутали slice и splice, или случайно мутировали данные там где не нужно, или наоборот?


            Сколько времени ушло на ревью вашего самописного debounce, сколько времени для написания unit тестов для debounce (а это слегка сложнее чем обычные pure функции, изо задействовано время)


            А вдруг с вашим debounce все норм, вы его используете в разных местах по проекту и получаете новую задачу, где вам нужно воспользоваться тем что в lodash debounce есть (к примеру maxTime) а в вашей ПРОСТОЙ реализации по аналогии с сайта you-dont-need- нет?

              0
              У нас всё как вы описали от начала и до
              Сколько человеко-QA-часов вы потеряете из-за того что перепутали slice и splice

              Собственно, наши специалисты не путают slice и splice.
              А так нам нормально и без lodash.
            0

            Что за надуманный пример? Мало того, что однобуквенные переменные, так еще и коллбэк в reduce принимает пять аргументов (чтобы избежать объявления переменной внутри него), хотя туда передается только четыре.


            Вот это, по-вашему, не является заменой в несколько понятных строк кода:


            var grouped = ['one', 'two', 'three'].reduce((accumulator, element) => {
                var key = element.length;
                if (!accumulator[key]) {
                    accumulator[key] = [];
                }
                accumulator[key].push(element);
                return accumulator;
            }, {});

            ?

              +1
              Что за надуманный пример?

              Я его взял с сайта you-dont-need-lodash который в самом начале этой ветки упоминался.


              Вот это, по-вашему, не является заменой в несколько понятных строк кода:

              Спорно, увидев такое в коде мне понадобится наверное в 2-10 раз больше времени чтоб прочитать/осознать чем на _.groupBy(arr, 'length')


              А уж если будет баг в функции в которой это используется внутри, то мне еще и придется потратить время чтоб дебажить внутренность этой функции

                +2
                Вот это, по-вашему, не является заменой в несколько понятных строк кода

                Если это встречается раза 2-3 в коде и в целом это и есть вся задача, то да, нет реальной нужды тащить что-то удобнее. Если же у вас повсеместно разные хитрые трансформации данных, то разумеется использование _.keyBy, _.groupBy, _.mapValues и многих других методов сильно упростит восприятие вашего кода.


                И да, в принципе вы можете написать свои собственные _.keyBy, _.groupBy, etc методы и сложить их куда-нибудь в ~/helpers/collections.ts. Но если через полгода плотной работы с вашим проектом вы заметите что ваш файл разросся до десятка другого килобайт нетестированного кода, то вы наверное вынужденно согласитесь, что не стоило артачиться с самого начала :D


                Особенно если учесть что современные бандлеры умеют подключать только задействованную часть кода либы.


                P.S. при попытке типизировать код выше — его читаемость сильно упадёт. А для groupBy есть готовые типы.

              0
              Практически весь lodash заменим нативным Javascript функционалом

              Вообще любая JS библиотека заменима "нативным Javascript функционалом". Она ведь на JS написана. Давайте не будем использовать NPM! Железная логика :)


              Lodash покрывает убогость стандартной библиотеки JS. Я не вижу смысла избегать его в пользу своих велосипедов в проектах среднего и большого размера (за исключением кейсов когда ваш велосипед гораздо круче решает поставленную задачу, разумеется).

                +1

                Убогость стандартной библиотеки покрывает babel. А поддержка IE8 не стоит ухудшения метрик для всех девайсов, в особенности мобильных.

                  +1
                  Убогость стандартной библиотеки покрывает babel

                  ??? причём тут babel? esnext не лечит убогость стандартной библиотеки. Она убога, что с ним, что без него.

              0
              Объясните, пожалуйста, почему?
                +2

                Почему что? ) Почему я рекомендую не писать import { get } from 'lodash/get'? Ну потому что в реальных проектах это легко превращается что половина всех ваших импортов в приложении это копипаста импортов методов лодеша. К чему весь этот мусор если у вас и без того подключён какой-нибудь babel и можно всё сделать гораздо удобнее?

              0
              >> Но может случиться так, что, импортировав всю библиотеку, вызовут лишь 4-5 функций. Это приводит нас к вопросу о целесообразности импорта всего пакета в ситуации, когда использовано будет лишь 2-3% его возможностей

              Тут не стоит забывать, что импортировав только 4-5 функций мы можем в итоге получить те же самые 23 килобайта, как если бы импортировали всю библиотеку целиком, т.к. некоторые функции lodash тянут за собой вереницу других функций.
                0

                А вы попробуйте. Я думаю как бы вы не старались — больше 50% за 5 функций вы не утянете ;)

                0
                В этом плане мне всегда нравился подход Linux — на каждую маломальскую задачу (атомарную — в идеале) отдельное приложение или библиотека.
                  0

                  Есть фанаты и такого подхода (lodash.get).

                  –1
                  Именно поэтому мне проще самому написать эти 4-5 функций, чем тянуть либы, потом ковыряться в них. А сам и опыт получаешь, и творческие навыки развиваешь. Чаще всего нужны какие то простые вещи, которые самому недолго сделать.
                    +1
                    Написать, внезапно узнать, что они не всегда верно работают, написать тесты.
                    Ради опыта и экономии 23Кб…
                      0

                      23kb там, 23kb тут, и вот уже 50kb. А 50 kb javascript-а это уже довольно чувствительно для слабых мобильных процессоров. Да и интернет может внезапно лагануть. Так что не стоит недооценивать килобайты.

                        0

                        Эти 23Kb скорее всего будут необходимы для первоначального рендеринга и инициализации всего проекта. Врядли вы сможете настроить lodash так, чтобы он действительно нужен был не сразу, а подгружался динамически (как, например, можно сделать с плагином для автокомплита).


                        А 23Kb — это 10-20% от первоначального бандла напрямую определяющего Time to Interactive метрику.

                          +1
                          Так вопрос стоимости:
                          — потратить, пусть, два часа на каждую самописную функцию или использовать lodash с проигрышем в 10% TTI
                          — потерять два часа на каждую самописную функцию или использовать альтернативное готовое решение c неполучением «опыта».

                          Мне кажется, среди этих вариантов нечто самописное проигрывает всегда, если только ты не совсем зеленый junior, пишущий свой pet-проект.
                          Уж между lodash и альтернативой я буду смотреть на то, когда нужно было реализовать фичу и на насколько нагруженном участке будут использоваться необходимые функции.
                            +2

                            Там ещё 1 аргумент в пользу lodash есть. Он уже давно суперпопулярен и большинство разработчиков с ним уже работало, и знает его API. А вот у своих велосипедов будет другое API. Всем новичкам придётся к ним привыкать и наступать на грабли с нуля. Скорее всего никакой документации не будет, хорошо если хотя бы типы будут.

                              0

                              Как можно проигрыш в 10% вообще оправдывать самописностью?

                              0

                              Если вы всё сделали правильно и импортировали 23 KiB lodash-а, то есть вероятность что ваши собственные велосипеды отъели бы 10-30 KiB сами по себе. То же мне выгода :)

                                –1

                                Ну, с таким же успехом можно придумать что угодно. Подключить plotly и говорить, что 23KiB фигня по сравнению с 600KiB.
                                А пользователи с мобильных устройств будут тем временем сажать батарейку.

                                  +1

                                  23 KiB vs 10-30 KiB (1/2...3/2)
                                  23 KiB vs 600 KiB (1/30)


                                  Что вы хотели этим сказать? И ещё уточню — вы отдаёте себе отчёт что 1 картинка из какого-нибудь слайдера часто весит 300 KiB. И никакой gzip её уже не сожмёт (в отличие от lodash-а).


                                  Какой-то фанатизм, ей богу. Если вы хотите оптимизировать скорость загрузки вашего приложения — начните с правильного конца. А для лодеша достаточно подключить babel-plugin-import. Вы легко можете выйти даже в плюс по размеру (на больших проектах).

                              0
                              Ради опыта и экономии 23Кб

                              Ну круто же. Я серьёзно. Опыт дорогого стоит. Можно безбоязненно тащить из lodash всё когда можешь его сам с нуля весь переписать. Экономит кучу времени. Но в рамках "поучиться" — хороший тренажёр. Ещё и готовые тесты можно взять.

                            +3

                            С приходом typescript 3.7.4 появился optional chaining. На мой взгляд get больше не нужен, как и многие ситуации, где применяли тернарные операторы и однострочные if'ы
                            можно писать так:


                                 employee?.address?.contact//_.get(employee, "address.contact", []
                                 //или
                                 employee?.address?.contact ?? []
                              +3

                              Всё так. Особенно это радует в связке с typescript, т.к. _.get возвращает any, а .? сохраняет все типы.

                              0
                              Нормальные библиотеки поддерживают tree-shaking через sideEffects: false в package.json и инструкций /* @__PURE__ */
                                0

                                Вроде бы у material в доке написано, что можно использовать именованные импорты безопасно если вебпак настроен нормально. Думаю в cra он настроен с поддержкой tree shaking. Поправьте если я не прав
                                https://material-ui.com/ru/guides/minimizing-bundle-size/#when-and-how-to-use-tree-shaking

                                  +1

                                  Эм… А может хватит смотреть на moment и ждать, пока его разобьют на части? :)
                                  Давным давно есть прекрасные date-fns и luxon (последний, кстати, от авторов moment)

                                    +1
                                    Конкретно с Lodash — мимо. Для ES-модулей и tree shaking есть lodash-es.

                                    А это использовать нельзя при правильно сконфигурированном межмодульном взаимодействии, т.к. у lodash один экспорт — default:

                                    import { get } from 'lodash';


                                      +1
                                      Вот это
                                      _.get(employee, "address.contact", []);

                                      убивает все автокомплиты и проверки типов в IDE. Ну а с приходом оператора .? вовсе перестает быть нужным.

                                      Импорт отдельных функций — вполне понятная самоцель, главное, чтобы она не выходила за рамки разумного. Ну а вопрос «своё или подключать обширную либу» стоит всегда и в любом языке. Обе крайности плохи — и подключение либы ради одной-двух простых функций, и упорная реализация собственных велосипедов для всего подряд.

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

                                      Самое читаемое