Ярким примером пакета, который импортируют целиком, нуждаясь лишь в нескольких его функциях, можно назвать библиотеку 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-проектах, боретесь с импортом ненужного кода?

faiwer
Просто оставлю это здесь (babel-plugin-import). Не пишите
import X from 'lib/X'
, ей богу. Зачем вам этот boilerplate.Carduelis
А еще я бы добавил вот этот репозиторий из серии you-dont-need-%library%.
Иногда вместо танцев с бубном стоит этот бубен не использовать.
Практически весь lodash заменим нативным Javascript функционалом.
Moment.js можно заменить альтернативной, браузер-ориентированной библиотекой.
А Material-UI стал поддерживать tree-shaking из-коробки.
knotri
Иногда действительно есть адекватные замены в несколько ПОНЯТНЫХ строк, а иногда:
Я бы на код ревью эту кашу из магических r,v,i,a,k не пропустил
Carduelis
Ключевое слово здесь "иногда". А если бездумно тащить каждую популярную библиотеку на initial load, никакие метрики скорости не пройдут. Конечно, если писать под electron, то можно и Moment.js тянуть без tree-shaking.
А придумывать в качестве аргумента задачи защищающие lodash — довольно странно.
knotri
Пусть вы начали писать новый проект, понадобился 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- нет?
kocherman
У нас всё как вы описали от начала и до
Собственно, наши специалисты не путают slice и splice.
А так нам нормально и без lodash.
Tazdingo1337
Что за надуманный пример? Мало того, что однобуквенные переменные, так еще и коллбэк в
reduce
принимает пять аргументов (чтобы избежать объявления переменной внутри него), хотя туда передается только четыре.Вот это, по-вашему, не является заменой в несколько понятных строк кода:
?
knotri
Я его взял с сайта you-dont-need-lodash который в самом начале этой ветки упоминался.
Спорно, увидев такое в коде мне понадобится наверное в 2-10 раз больше времени чтоб прочитать/осознать чем на _.groupBy(arr, 'length')
А уж если будет баг в функции в которой это используется внутри, то мне еще и придется потратить время чтоб дебажить внутренность этой функции
faiwer
Если это встречается раза 2-3 в коде и в целом это и есть вся задача, то да, нет реальной нужды тащить что-то удобнее. Если же у вас повсеместно разные хитрые трансформации данных, то разумеется использование
_.keyBy
,_.groupBy
,_.mapValues
и многих других методов сильно упростит восприятие вашего кода.И да, в принципе вы можете написать свои собственные
_.keyBy
,_.groupBy
, etc методы и сложить их куда-нибудь в~/helpers/collections.ts
. Но если через полгода плотной работы с вашим проектом вы заметите что ваш файл разросся до десятка другого килобайт нетестированного кода, то вы наверное вынужденно согласитесь, что не стоило артачиться с самого начала :DОсобенно если учесть что современные бандлеры умеют подключать только задействованную часть кода либы.
P.S. при попытке типизировать код выше — его читаемость сильно упадёт. А для groupBy есть готовые типы.
faiwer
Вообще любая JS библиотека заменима "нативным Javascript функционалом". Она ведь на JS написана. Давайте не будем использовать NPM! Железная логика :)
Lodash покрывает убогость стандартной библиотеки JS. Я не вижу смысла избегать его в пользу своих велосипедов в проектах среднего и большого размера (за исключением кейсов когда ваш велосипед гораздо круче решает поставленную задачу, разумеется).
Carduelis
Убогость стандартной библиотеки покрывает babel. А поддержка IE8 не стоит ухудшения метрик для всех девайсов, в особенности мобильных.
faiwer
??? причём тут babel? esnext не лечит убогость стандартной библиотеки. Она убога, что с ним, что без него.
DenniLa2
Объясните, пожалуйста, почему?
faiwer
Почему что? ) Почему я рекомендую не писать
import { get } from 'lodash/get'
? Ну потому что в реальных проектах это легко превращается что половина всех ваших импортов в приложении это копипаста импортов методов лодеша. К чему весь этот мусор если у вас и без того подключён какой-нибудь babel и можно всё сделать гораздо удобнее?