Кажется, мы стали забывать основы фронтенда
Под капотом у типичного фронтенд-проекта
Обычно, в подобных статьях я выступаю на позитивной стороне и говорю, что всё не так уж плохо, что умелыми руками можно делать прекрасные вещи. Однако, недавно со мной произошло несколько историй, которые заставили меня пересмотреть свой взгляд на ситуацию в индустрии.
В этой статье я перескажу истории с некоторыми техническими деталями и порассуждаю, что делать дальше.
История #1: за чистый CSS
Есть у нас компонент Container, у которого может быть опциональный футер. Когда он есть — то мы рендерим дополнительную обертку для него, с отступами и обводками.
function Container({ footer, children }) {
return
{children}
{footer && {footer}}
}
В простых вариантах все работает гладко, вот тут
футера не будет, а здесь
— будет. Проблемы начинаются, когда контент футера динамический:
. Компонент Footer может вернуть контент, а может и null
, но наше условие {footer && }
об этом не знает, и может иногда рендерить пустой div.
Один разработчик попытался поправить эту ситуацию. Он подумал –, а что, если мы проверим содержимое div и спрячем его?
function Container({ footer, children }) {
const footerRef = useRef();
useEffect(() => {
const hasContent = footerRef.current.childNodes.length > 0;
footerRef.current.style.display = hasContent ? 'block' : 'none';
});
return
{children}
{footer && {footer}}
}
Другой разработчик пришел на code review и заметил, что этот код работает только при первом рендере. Если футер обновляется асинхронно, то useEffect не вызовется, и обновления не произойдет. Разработчики посовещались и решили копать в сторону MutationObserver.
В процессе обсуждений они так же решили уточнить у меня, что я думаю про этот подход. А мне вспомнился анекдот про NASA, которые потратили миллионы долларов на разработку шариковой ручки для условий невесомости, а советские космонавты просто взяли с собой карандаши.
«Простое советское» решение
Достаточно было просто воспользоваться CSS-селектором :empty
.footer:empty {
display: none;
}
Разработчики настолько привыкли решать задачи с помощью JS, что у них даже мысли не возникло, чтобы посмотреть, что там есть в CSS
История #2: как загружать скрипты
Есть у нас ещё один виджет, боковая панель, которая должна растягиваться во всю высоту, но не перекрывать хедер и футер. Примерная формула получается такая: 100% - headerHeight - footerHeight
.
Решение работало гладко на всех страницах, кроме одной. Там почему-то headerHeight
считался правильно, а вот footerHeight
возвращал 0. Разработчик, которому досталась эта задача, поковырял глубже и выяснил, что document.querySelector('footer')
возвращает null
в этом случае, хотя позже футер на странице всё равно загружается. Какая-то мистика, подумал он и решил что надо перехватить момент его появления через MutationObserver. Это другой разработчик, не из первой истории, хотя костыль абсолютно такой же.
Мне это показалось странным, и я решил поискать альтернативное решение. И нашел его, достаточно было поменять местами пару строк кода…
Вот эти строки
Вот HTML этой страницы:
Каким-то образом скрипт оказался на странице раньше футера. Поскольку он исполняется синхронно, то в момент рендеринга футер еще не прогрузился, поэтому его высота и не считалась. Стоило поменять эти две строчки местами, и всё заработало как надо.
А разработчики, избалованные современными инструментами сборки, уже не пишут HTML руками, надеются на помощь html-webpack-plugin и т.п. Поэтому когда внезапно оказывается нужно написать немного HTML самостоятельно, то они тут же пасуют. Хотя казалось бы, что тут сложного?
История #3: корень всех зол
Реакт версии 16.8 подарил миру hooks API, а вместе с ним и огромное поле раскиданных граблей. Если прочесть документацию и понять что к чему, то писать вроде бы несложно, но из-за наличия хуков useMemo
и useCallback
теперь каждый джуниор мнит себя богом оптимизаций и вставляет их по поводу и без.
Посмотрим на такой пример. Есть компонент календаря, в котором нужно генерировать 2D-массив для отображения дат в текущем месяце. Вот примерный код:
import { getCalendarMonth } from 'mnth';
function Calendar({ date }) {
const month = getCalendarMonth(date);
// код рендеринга условный, просто чтобы показать структуру
return month.map((week) => (
{week.map((day) => (
{day.getDate()}
))}
));
}
getCalendarMonth
не особо тяжелая функция, но у разработчика все равно зачесались руки её заоптимизировать:
const month = useMemo(() => getCalendarMonth(date), [date])
Но такая оптимизация не работает, потому что объект date может быть другим инстансом, содержащим тоже время, а useMemo сравнивает объекты в лоб. Поэтому нужно извлечь timestamp:
const timestamp = date.getTime();
const month = useMemo(
() => getCalendarMonth(date),
// eslint плагин ругается на то что мы используем не тот объект
// в зависимостях, поэтому нужно добавить исключение
// eslint-disable-next-line react-hooks/exhaustive-deps
[timestamp]
);
Тут возникает вопрос –, а принесли ли эти выверты хоть какую-то пользу?
Давайте померим
Я воспроизвел ситуацию в этом демо: https://ethereal-rain-forger.glitch.me
Один список рендерится с мемоизацией, а другой — нет. В консоли пишется время затраченное на рендер. В обоих случаях это порядка одной милисекунды. А если не видно разницы, зачем плодить сущности и устраивать цирк с псевдо-оптимизациями?
Что же делать?
Ситуация удручающая. Разработчики усложняют решения на ровном месте и считают это абсолютно нормальным. Во всех приведенных ситуациях после моих предложений они всё-таки переделали код на более простую версию, но это потому что я на это указал, а сколько еще мест прошли мимо меня. За всем не уследить.
Поэтому если вы начинающий разработчик (да и всем остальным тоже), у меня для вас есть простые советы:
Даже если вы разрабатываете на фреймворке, потратьте время, разберитесь с vanilla js. Посмотрите, как оно ведет там себя под капотом, и будете увереннее разбираться, когда что-то работает не так ожидалось.
Учите CSS. Там есть очень много полезных свойств и селекторов, которые заменят вам тонны JS. «Я использую готовую дизайн-библиотеку» — это не ответ, под капот нужно заглядывать всегда, см. пункт 1.
Развивайте критическое мышление — ваш тимлид/ментор скорее всего научил вас определенным хорошим практикам. Но одно слепо им следовать «а то будет атата» и совсем другое разобраться, почему именно так сложилось, и что именно будет не работать если так не делать.
Помните про YAGNI, KISS и другие принципы. Если простая задача оборачивается запутанным решением, притормозите, посмотрите на неё с другой стороны, может быть вы слишком углубились в одну гипотезу, и забыли о чем-то очевидном.
Ну и помните, состояние ваших проектов зависит только от вас. Все инструменты уже давно имеются — надо только не забывать ими пользоваться.