Как стать автором
Обновить

Кажется, мы стали забывать основы фронтенда

Время на прочтение 5 мин
Количество просмотров 45K
Под капотом у типичного фронтенд-проекта
Под капотом у типичного фронтенд-проекта

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

В этой статье я перескажу истории с некоторыми техническими деталями и порассуждаю, что делать дальше.

История #1: за чистый CSS

Есть у нас компонент Container, у которого может быть опциональный футер. Когда он есть – то мы рендерим дополнительную обертку для него, с отступами и обводками.

function Container({ footer, children }) {
  return <div>
    {children}
    {footer && <div className="footer">{footer}</div>}
  </div>
}

В простых вариантах все работает гладко, вот тут <Container footer={null} /> футера не будет, а здесь <Container footer="View more" /> – будет. Проблемы начинаются, когда контент футера динамический: <Container footer={<Footer />}>. Компонент Footer может вернуть контент, а может и null, но наше условие {footer && <div />} об этом не знает, и может иногда рендерить пустой 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 <div>
    {children}
    {footer && <div ref={footerRef} className="footer">{footer}</div>}
  </div>
}

Другой разработчик пришел на 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>
<head></head>
<body>
  <header></header>
  <main id="app"></main>
  <script src="app.js"></script>
  <footer></footer>
</body>
</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) => (
    <div>
      {week.map((day) => (
        <span>{day.getDate()}</span>
      ))}
    </div>
  ));
}

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 и другие принципы. Если простая задача оборачивается запутанным решением, притормозите, посмотрите на неё с другой стороны, может быть вы слишком углубились в одну гипотезу, и забыли о чем-то очевидном.

Ну и помните, состояние ваших проектов зависит только от вас. Все инструменты уже давно имеются – надо только не забывать ими пользоваться.

Теги:
Хабы:
+96
Комментарии 191
Комментарии Комментарии 191

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн