Лучшие стратегии разработки фронтенда в 2022 году +29


AliExpress RU&CIS

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

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



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

Сколько ядер имеет процессор компьютера или смартфона?


Все мы видели изображения процессоров, вроде того, что показано ниже.


Например, если вы работаете на Mac, вы можете щёлкнуть на значке яблока в верхнем левом углу рабочего стола, далее — выбрать пункт меню About This Mac, после чего вам будет показано нечто вроде Processor 3,2 GHz 8-Core Intel Xeon W.

У процессора iPhone имеется 6 ядер.

Короче говоря, в распоряжении каждого компьютера или смартфона имеется несколько процессорных ядер.

Это значит, что вы можете выполнять на этих ядрах, в параллельном режиме, несколько потоков.

Если бы вы создавали автомобиль — оснастили бы вы его двигателем, в котором имеется лишь один цилиндр?

Если ваш ответ будет звучать как «Конечно нет! Это будет очень медленный автомобиль!», тогда вам стоит внимательно читать эту статью.

Сколько процессорных ядер использует браузер?


Браузер, сам по себе, использует лишь одно ядро на одну вкладку или на одно окно.

Это значит, что ваше Angular- или React-приложение выглядит примерно так:


Чем больше JavaScript-задач выполняется в вашем приложении — тем медленнее оно будет работать. Самое худшее, что может случиться, выглядит как полное «подвисание» интерфейса, когда единственное доступное вам процессорное ядро оказывается полностью загруженным. А вот другие его ядра в это время, вполне возможно, будут совершенно ничем не заняты.

И это всё совершенно не масштабируется.

Попутно замечу, что если вы создаёте простые, маленькие, а ещё лучше — статические веб-сайты или приложения — этих возможностей вам должно вполне хватить.

API Web Workers


Вот что пишут о веб-воркерах на MDN:
Web Workers это механизм, который позволяет скрипту выполняться в фоновом потоке, который отделен от основного потока веб-приложения. Преимущество заключается в том, что ресурсоёмкие вычисления могут выполняться в отдельном потоке, позволяя запустить основной (обычно пользовательский) поток без блокировки и замедления.
А вот что о них сказано в Википедии:
W3C и WHATWG представляют себе веб-воркеры в виде скриптов, выполняющихся длительное время, выполнение которых не прерывается скриптом, ответственным за обработку щелчков мышью или других механизмов взаимодействия пользователя с приложением. То, что работа подобных скриптов не может быть прервана действиями пользователя, должно способствовать тому, что веб-страницы остаются отзывчивыми в то же самое время, когда они выполняют длительные задачи в фоновом режиме.

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

Давайте вдумаемся в следующие слова:

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

Они ведут нас к вопросу: «Что является самой сложной вычислительной задачей?».

Ответ на этот вопрос очень прост: это выполнение кода самих UI-фреймворков или библиотек, а так же кода тех приложений, которые мы создаём с их использованием.

Это приводит нас к следующей идее: давайте переместим в воркеры из главного потока всё, что можно. Это позволит главному потоку сосредоточиться исключительно на тех задачах, для выполнения которых он и создан, а именно — на манипулировании DOM.

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

Эта идея ведёт нас к концепции, приведённой в заголовке следующего раздела.

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


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


Самое худшее, что теперь может произойти, случится тогда, когда воркер приложения замедлится, а нагрузка на ядро, на котором он работает, дойдёт до 100%. Но это не повлияет на пользовательский интерфейс приложения, так как главный поток — это поток, отвечающий за рендеринг интерфейса.

Возможно, наилучшее решение для одностраничных приложений (single page apps, SPA) выглядит так, как показано ниже.


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

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

В этом материале можно почитать о том, как сделать использование воркера необязательным.

Может ли воркер работать с DOM?


В глобальной области видимости воркера (WorkerGlobalScope) не определены переменные window и window.document.

Это значит, что из воркера нельзя напрямую обратиться к реальной DOM.

В результате для организации работы с DOM из воркера у нас есть 2 варианта.

Первый — это воссоздание всего DOM API внутри воркера. Полагаю, что это — плохая идея. Воркеры не без причины отделены от DOM, с DOM связано огромное количество постоянно меняющейся логики. Операции по взаимодействию с DOM становятся асинхронными, и если вы последовательно выполняете множество таких операций, это приведёт к тому, что воркер будет отправлять множество сообщений создавшему его потоку, используя метод postMessage(). Единственное преимущество такого подхода заключается в том, что код приложений можно писать так же, как его писали раньше, а это — спорное преимущество. Позже я расскажу о более приемлемом варианте решения этой задачи.

На самом деле, существует проект, worker-dom, в рамках которого реализована именно такая схема работы.

Второй вариант организации работы с DOM из воркера выглядит более разумным. Заключается он в том, чтобы придерживаться концепции, в соответствии с которой воркер ничего не должен знать о реальной DOM.

Это делает совершенно обязательным использование виртуальной DOM.

Мне встречались статьи, где часто проскакивает мысль о том, что виртуальная DOM — это плохо.

Но это попросту неправда. Ответ на вопрос о том, «хорошо» это или «плохо», сильно зависит от реализации виртуальной DOM.

Главными камнями преткновения в Angular и React являются шаблоны, основанные на XML или JSX. Их надо транспилировать, превращая в нечто такое, с чем мы можем работать.

А JavaScript — это язык, который не создавался ни в расчёте на то, что он будет очень быстрым, ни в расчёте на то, что он будет использоваться для парсинга строк.

Парсинг шаблонов — это настолько вычислительно сложная операция, что в наши дни снова стал популярным серверный рендеринг (server side rendering, SSR). Я занимался веб-разработкой 20 лет назад, создавая CMS, основанные на PHP, которые генерировали HTML-файлы на сервере.

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

Существуют ли исключительные ситуации, в которых воркер может получать доступ к DOM?


На самом деле — есть лишь одно исключение. Это — интерфейс OffscreenCanvas. Смысл тут в том, что воркер может получать контроль над узлами DOM, представляющими собой элементы canvas.

Этот механизм уже отлично работает в Chromium, в Safari (Webkit), а в Firefox идёт его активная реализация. Возможно, это займёт ещё месяцев шесть, в результате это — тема для публикаций 2022 года.

Возможно вы, на одной из предыдущих схем, заметили жёлтый блок с надписью Canvas Worker. Если тема работы с графикой за пределами главного потока вам интересна — взгляните на этот материал.

Каков разумный подход к созданию виртуальной DOM?


Хотя JavaScript — это и не лучший язык для разбора строк, он отлично показывает себя в деле работы с вложенными структурами, представленными объектами и массивами. У такого формата представления данных есть название, с которым вы, определённо, знакомы: JSON.

Если использовать синтаксис работы с виртуальной DOM, основанный на JSON, тогда в UI-коде не нужно будет постоянно выполнять достаточно тяжёлые задачи парсинга шаблонов, или даже переносить решение таких задач на то время, когда осуществляется сборка проекта.

Это, определённо, аналогично прямой работе с тем, что получается после парсинга JSX-шаблонов.

Если при таком подходе всё организовано правильно — то в виртуальной DOM не будет ни переменных, ни выражений if/else, ни привязок, ни методов, ни циклов, ни какой-либо логики. И, если вспомнить Angular, вы никогда не столкнётесь с шаблонами, состоящими из более чем 1000 строк кода.

С применением программного подхода логика будет использоваться там, где ей и место: в JavaScript-коде. Например, при создании списка можно сначала построить скелет этого списка для виртуальной DOM, а после того, когда из хранилища поступят данные для этого списка, обойти записи, на ходу создавая узлы виртуальной DOM.

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

Упрощается реализация бесконечной прокрутки или других продвинутых возможностей.

Много интересного на эту тему можно найти здесь и здесь.

И, хотя применение программного подхода имеет смысл для реализации низкоуровневых операций с виртуальной DOM, мы, определённо, предпочитаем, при создании программ, использоваться декларативным подходом.

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

Смысл тут в том, что работать с виртуальной DOM нужно будет лишь при создании классов компонентов. А при создании приложений можно просто пользоваться деревом компонентов.

Можно ли сделать так, чтобы интерфейсы приложений были бы работоспособны в браузерах без необходимости их дополнительной обработки?


Когда 5 или 8 лет назад стала популярной библиотека React, браузеры не лучшим образом показывали себя в деле поддержки самых свежих возможностей, описанных в стандарте ECMAScript.

Например — там не было поддержки классов (ES6) или JS-модулей.

В тот момент имело смысл перевести решение задач UI-разработки на платформу Node.js.

Речь идёт о том, что можно использовать последние возможности языка, и, тратя некоторое время на сборку проекта, компилировать или транспилировать свой код в JavaScript, понятный браузерам.

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

Если говорить о воркерах, то, например, в Chromium они хорошо поддерживают использование JS-модулей. Разработчики Webkit (Safari) завершают работу над аналогичной возможностью, но она пока доступна лишь в версии браузера Safari Technology Preview. Mozilla (Firefox) активно работает над этой технологией.

Можно с высокой долей уверенности предположить, что полная поддержка модулей в воркерах появится в 2022 году.

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

Такой подход имеет очевидные сильные стороны:

  1. JavaScript должен быть единственным языком программирования, который понимают движки браузеров.
  2. Написание JS-кода таким образом, что он оказывается непонятным браузеру, не считается чем-то нормальным.
  3. Перенеся UI-разработку обратно в браузер, мы можем отлаживать реальный код без применения сборщиков и транспиляторов, без использования карт кода.
  4. Нет нужды в замене механизмов работы с модулями.

В частности, написание и отладка кода снова станут приятными занятиями, так как мы будем уверены в том, что работаем именно со своим кодом, и что некие внешние факторы не являются источником ошибок.

Инструменты вроде webpack, определённо, всё ещё нужны для создания продакшн-вариантов приложений. Но они будут представлять собой инструменты для сборки проектов, а не части сред выполнения кода.

Переход с Node.js на Deno ещё сильнее подстегнёт это движение. CommonJS исчезнет — скорее рано, чем поздно. А после того, как в Deno появится менеджер пакетов, всё больше и больше пакетов будут использовать синтаксис, который позволит им работать в браузерах (то есть — отказ от использования простых спецификаторов модулей, отсутствие импортов с путями, не поддерживаемыми браузерами, отсутствие расширений файлов).

Есть ли будущее у TypeScript?


Это, вероятно, самый спорный раздел данной статьи. Сообщество JS-разработчиков раскололось пополам: одним TS нравится, а другие и близко к нему не подходят. И я с нетерпением ожидаю появления новых аргументов в их споре.

Сам я думаю, что прямо сейчас, при использовании для разработки интерфейсов возможностей Node.js, и, в любом случае, не имея возможности обойтись без сборки или транспиляции проекта, пользоваться TS — это совершенно нормально.

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

Станете ли вы добавлять в рабочий процесс дополнительный шаг сборки проекта только ради использования TS?

В этот момент применение TS окажется слишком дорогим удовольствием.

Самое важное тут то, что TypeScript — это не веб-стандарт. И нет даже планов по реализации поддержки TS браузерами.

История, совершенно определённо, уже несколько раз показывала нам, что происходит с веб-технологиями, которые не основаны на веб-стандартах. А именно — такие технологии просто в определённый момент исчезают. Отличный пример этого — Microsoft Silverlight.

Проверка типов — это, в целом, хорошая вещь. Главная проблема заключается в том, что Angular и React попросту не используют комментарии, основанные на JSDoc, что позволяло бы IDE выдавать предупреждения в процессе написания кода.

Но, на самом деле, можно «обмануть» TS, пользуясь JSDoc-комментариями в обычных JS-файлах.

Это, определённо, интересная возможность, о которой можно и поговорить.

Если вам и правда очень нужна поддержка проверки типов в языке, которым вы пользуетесь, и шаг сборки проекта вас не особенно беспокоит, не окажется ли Dart 2 именно тем, что вам нужно?

В Dart 2 имеется полная поддержка воркеров, поэтому тут можно воспользоваться вышеописанной архитектурой приложения, в которой применяются воркеры. Этот язык, кроме того, хорошо подходит для разработки мобильных проектов, в число его плюсов входит AOT-компиляция кода.

Что не так с React?


Буду честен: ещё до того, как появилась библиотека React, существовала библиотека jQuery. Когда библиотека React стала популярной, это стало для веб-разработки большим шагом вперёд. Это была первая библиотека, которая открыла множеству разработчиков путь к применению виртуальной DOM.

Почему бы нам не использовать React в 2022 году? У этой библиотеки есть некоторые недостатки:

  1. Код React выполняется в главном потоке.
  2. Кодовая база React основана на CommonJS, в результате код этой библиотеки не может выполняться в браузере без предварительной сборки React-проекта.
  3. Отсутствие поддержки JSDoc-комментариев.
  4. Парсинг JSX-шаблонов весьма ресурсозатратен. Есть даже компиляторы, вроде Svelte, направленные на то, чтобы переложить решение этой задачи на сервер.
  5. React не даёт доступа к механизмам своего ядра. Всё в нём, непонятно почему, выглядит как расширение Component.
  6. Управление состоянием приложений устроено, без всякой на то причины, слишком сложно.
  7. Метод render() весьма неоднозначен.

Хочу подробнее остановиться на последнем пункте этого списка. А именно, речь идёт о том, что сложно сделать так, чтобы изменения состояния не вызывали бы render(). Если React-компонент содержит дочерние компоненты (собственные теги в render()), то, если недостаточно разумно пользоваться keys, будут создаваться новые экземпляры компонентов.

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

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

React — это всего лишь библиотека, а не фреймворк. То есть — Component — это, в общем-то, всё, что даёт нам React. Тут нет логических иерархических связей наподобие следующей:

core.Base -> component.Base -> button.Base -> tab.header.Button

После того, как разработчик справился с безумием render(), он может выбрать наиболее подходящий базовый класс для того, что хочет создать. Например, у класса Container есть объект vdom, который содержит ссылки на объекты виртуальной DOM своих дочерних элементов. Это позволяет менять виртуальную DOM дочерних компонентов, не пересоздавая их экземпляры, описанные средствами JS.

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

Многооконные приложения


Если поменять вышеописанную архитектуру приложений, использующих воркеры, и добавить в неё воркер, который может работать с различными вкладками или окнами браузера (Shared App Worker), это ещё сильнее расширит наши возможности.


Это позволит нам перемещать целые деревья компонентов между различными окнами браузера, не теряя при этом соответствующих им JS-экземпляров компонентов.

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

При таком подходе, например, можно перетаскивать объекты между разными окнами.

Если вам это интересно — существуют публикации на эту тему, которые вы можете найти самостоятельно.

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


Самостоятельная реализация всего того, о чём мы говорили, может, в буквальном смысле, занять несколько лет.

Но если вам интересен этот подход — знайте, что самим вам всё это реализовывать не придётся. Вы вполне можете воспользоваться моими наработками, представленными проектом neo.mjs. В него уже сделано более 12000 коммитов, он выпущен под лицензией MIT.

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

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

Для поддержки различных архитектур, вроде MVVM, или архитектур, основанных на наблюдаемых объектах, не требуется использование сторонних библиотек.

Тут, кроме того, очень просто устроено управление состоянием (с использованием классов).

Вот страница, на которой собраны примеры neo.mjs-приложений и публикации об этом фреймворке.

Для этого фреймворка созданы инструменты командной строки, с помощью которых, например, можно создать новый проект (рабочее пространство), выполнив одну простую команду: npx neo-app. Neo.mjs поддерживает объединение фрагментов приложений, что ведёт к тому, что при размещении нескольких приложений на одной странице практически не создаётся дополнительная нагрузка на систему.

Итоги


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

Правда, к сожалению, многие разработчики всё ещё даже не знают о существовании проекта neo.mjs.

И да, я буду рад, если кто-то попытается разубедить меня в правильности всего того, о чём я тут рассказал. Для того чтобы это сделать, вам нужно будет создать своё первое neo.mjs-приложение, на котором вы сможете проверить мои идеи. Я, в таком случае, с удовольствием проведу код-ревью вашего проекта. В заключение хочу сказать, что neo.mjs — это самое быстрое из существующих средств, позволяющих манипулировать DOM во время работы кода. Особенно — если речь идёт о больших и сложных приложениях.

Планируете ли вы попробовать neo.mjs?




Комментарии (25):