Проверяем импорты через ESLint
В процессе улучшения структуры проекта, которую я описывал здесь, задумался над базовыми проверками корректности импортов. Эта структура строится на «функциональном мышлении» (далее ФМ), в котором код должен быть разделён на данные, вычисления и действия, поэтому необходимо проверять корректность импортов между определёнными сегментами. Например, чтобы сегмент ui (в контексте ФМ это вычисления) не мог импортировать сегмент $api (в контексте ФМ это действия). На самом деле причин и проверок больше, но не суть.
Для эксперимента был выбран eslint-plugin-boundaries. В целом неплохой инструмент, хотя сильно не хватает разделения контекста, но разработка в этом направлении вроде как ведётся. В моём случае, например, необходимы разные правила для проверки верхнего уровня папок и уровня сегментов.
eslint.config.js
eslint-boundaries.js
Полная версия файла тут.
Да, все возможные варианты этот код не покрывает, но для начала этих проверок вполне достаточно.
В процессе улучшения структуры проекта, которую я описывал здесь, задумался над базовыми проверками корректности импортов. Эта структура строится на «функциональном мышлении» (далее ФМ), в котором код должен быть разделён на данные, вычисления и действия, поэтому необходимо проверять корректность импортов между определёнными сегментами. Например, чтобы сегмент ui (в контексте ФМ это вычисления) не мог импортировать сегмент $api (в контексте ФМ это действия). На самом деле причин и проверок больше, но не суть.
Для эксперимента был выбран eslint-plugin-boundaries. В целом неплохой инструмент, хотя сильно не хватает разделения контекста, но разработка в этом направлении вроде как ведётся. В моём случае, например, необходимы разные правила для проверки верхнего уровня папок и уровня сегментов.
eslint.config.js
import { defineConfig } from 'eslint/config';
import parser from '@typescript-eslint/parser';
import boundaries from './eslint-boundaries.js';
export default defineConfig([
{
extends: [boundaries],
files: ['src/**/*.{ts,tsx}'],
languageOptions: { parser },
settings: { 'import/resolver': { typescript: true } },
},
]);
eslint-boundaries.js
import boundariesPlugin from 'eslint-plugin-boundaries';
export default {
plugins: {
boundaries: boundariesPlugin,
},
settings: {
'boundaries/elements': [
{ type: '$control', pattern: 'src/*/$control/**' },
{ type: '_core', pattern: 'src/_core/*', mode: 'full' },
// ...
],
},
rules: {
'boundaries/element-types': [
'warn', {
default: 'allow',
message: '[${file.type}] is not allowed to import [${dependency.type}]',
rules: [
{ from: '$control', disallow: ['$page'] },
{ from: '_core', disallow: ['_ui-kit', 'admin', 'app'] },
// ...
],
},
],
},
};
Полная версия файла тут.
Да, все возможные варианты этот код не покрывает, но для начала этих проверок вполне достаточно.
🔥2
Что за звери такие cohesion и coupling
Возможно, вы слышали, что при выборе архитектуры (или её проектировании) нужно стремиться к high cohesion (высокому сцеплению) и low coupling (низкой связанности). Так вот, cohesion и coupling — два базовых критерия оценки качества архитектуры.
Cohesion — это то, насколько части одного модуля логически относятся к одной задаче. Чем выше cohesion, тем больше «про одно» этот модуль, тем проще его понять и менять. Можно даже сказать, что high cohesion это принцип единой ответственности из SOLID.
Coupling — это то, насколько один модуль зависит от других. Чем ниже coupling, тем меньше модули знают друг о друге и тем легче менять их независимо.
В общем, хорошая архитектура, это такая, где каждый модуль или фича — самодостаточный «чёрный ящик» с высокой cohesion внутри (всё логически про одну задачу, без лишнего мусора) и низким coupling снаружи (минимальные зависимости), чтобы код легко масштабировался, менялся и тестировался без цепной реакции по всему проекту.
И тут вы справедливо можете заметить, что в реальном мире всё несколько сложнее, и да, вы будете абсолютно правы.
П.С. Дедовский лайфхак, как проверить, что у вас хорошая (или не очень) архитектура: попробуйте поудалять разные (конечные) модули вашей системы. Чем меньше изменений придётся вносить в оставшуюся часть системы, тем лучше архитектура.
Возможно, вы слышали, что при выборе архитектуры (или её проектировании) нужно стремиться к high cohesion (высокому сцеплению) и low coupling (низкой связанности). Так вот, cohesion и coupling — два базовых критерия оценки качества архитектуры.
Cohesion — это то, насколько части одного модуля логически относятся к одной задаче. Чем выше cohesion, тем больше «про одно» этот модуль, тем проще его понять и менять. Можно даже сказать, что high cohesion это принцип единой ответственности из SOLID.
Coupling — это то, насколько один модуль зависит от других. Чем ниже coupling, тем меньше модули знают друг о друге и тем легче менять их независимо.
В общем, хорошая архитектура, это такая, где каждый модуль или фича — самодостаточный «чёрный ящик» с высокой cohesion внутри (всё логически про одну задачу, без лишнего мусора) и низким coupling снаружи (минимальные зависимости), чтобы код легко масштабировался, менялся и тестировался без цепной реакции по всему проекту.
И тут вы справедливо можете заметить, что в реальном мире всё несколько сложнее, и да, вы будете абсолютно правы.
П.С. Дедовский лайфхак, как проверить, что у вас хорошая (или не очень) архитектура: попробуйте поудалять разные (конечные) модули вашей системы. Чем меньше изменений придётся вносить в оставшуюся часть системы, тем лучше архитектура.
🔥3🤔1
Знакомая всем вам двоичная система счисления, которая используется практически во всех современных компьютерах, была описана Г. В. Лейбницем в 17 веке. Важный момент тут в том, что она была именно описана, а не изобретена, ведь как известно, всё уже изобретено до нас. Идеи двоичной записи появились за долго до Лейбница, например в древнекитайской «Книге Перемен» о которой он точно знал, как человек увлекающийся китайской культурой. На минутку, возраст этой книги около 3000 лет! Книга Перемен — философский текст, включающий 64 символа, каждый из которых состоит из 6 черт (сплошных или прерывистых, что соответствует 1 и 0).
Хорошие идеи часто лежат на виду, просто в других плоскостях. Чем шире ваш кругозор, тем больше связей вы замечаете и тем оригинальнее становятся ваши решения.
Расширяйте свои горизонты!
С Новым годом!
Хорошие идеи часто лежат на виду, просто в других плоскостях. Чем шире ваш кругозор, тем больше связей вы замечаете и тем оригинальнее становятся ваши решения.
Расширяйте свои горизонты!
С Новым годом!
🎄4🔥1
Самые изменяемые файлы проекта, о чём говорят и как посмотреть
Частота изменения файлов — интересная метрика, способная подсветить места повышенных рисков, архитектурных проблем и некоторых других моментов. Для более полной картины происходящего, рекомендуется рассматривать её в совокупности с такой информацией, как количество строк в файле, сложность кода, связи между файлами, количество задач и их связанность.
Собрать список самых изменяемых файлов, можно, например, с помощью
Важная ремарка: не всегда полученный список будет отражать что-то полезное. Вполне может быть так, что в нём будут одни index, config, types и тому подобные файлы. Хотя, слишком частое изменение одного и того же index файла — тоже, своего рода, знак.
В моём случае, после беглого изучения файлов из полученного списка, первое что бросилось в глаза — размер: 670, 450, 490 и 390 строк кода. Многовато, особенно в первом из них. На нём и заострим своё внимание.
Из полученного списка коммитов, путём не хитрых манипуляций, извлекаем авторов, а главное — список задач, которых оказалось не мало, они имели слабую связь, а часть из них вообще была исправлением ошибок.
Не забывайте вставлять номера задач в каждый коммит, это помогает в подобных случаях, а также в случаях когда нужно понять, почему были внесены те или иные изменения.
Всё это: частота изменения файла, количество строк кода, количество и (не) связанность задач, в данном случае, явно сообщает нам о том, что это типичный «god-компонент» (в нашем случае хук) и первый кандидат на ближайший рефакторинг, а также требует особого внимание на ревью и даже в тестировании.
Частота изменения файлов — интересная метрика, способная подсветить места повышенных рисков, архитектурных проблем и некоторых других моментов. Для более полной картины происходящего, рекомендуется рассматривать её в совокупности с такой информацией, как количество строк в файле, сложность кода, связи между файлами, количество задач и их связанность.
Собрать список самых изменяемых файлов, можно, например, с помощью
git log.Важная ремарка: не всегда полученный список будет отражать что-то полезное. Вполне может быть так, что в нём будут одни index, config, types и тому подобные файлы. Хотя, слишком частое изменение одного и того же index файла — тоже, своего рода, знак.
git log master \
--name-only \
--pretty=format: \
--since="6.month" \
-- apps/***/src/ \
| sort | uniq -c | sort -nr | head -5
1564
53 apps/***/use-some-dialog.ts
42 apps/***/submit.tsx
31 apps/***/main.tsx
26 apps/***/some-content.tsx
В моём случае, после беглого изучения файлов из полученного списка, первое что бросилось в глаза — размер: 670, 450, 490 и 390 строк кода. Многовато, особенно в первом из них. На нём и заострим своё внимание.
git log master \
--no-merges \
--since="6.month" \
--pretty=format:"%ae %ad %s" --date=short \
apps/***/use-some-dialog.ts \
| head -100
Из полученного списка коммитов, путём не хитрых манипуляций, извлекаем авторов, а главное — список задач, которых оказалось не мало, они имели слабую связь, а часть из них вообще была исправлением ошибок.
Не забывайте вставлять номера задач в каждый коммит, это помогает в подобных случаях, а также в случаях когда нужно понять, почему были внесены те или иные изменения.
Всё это: частота изменения файла, количество строк кода, количество и (не) связанность задач, в данном случае, явно сообщает нам о том, что это типичный «god-компонент» (в нашем случае хук) и первый кандидат на ближайший рефакторинг, а также требует особого внимание на ревью и даже в тестировании.
❤7
Императив vs Декларатив. Точно понимаете разницу?
Императивный стиль описывает КАК достичь результата (пошаговые инструкции), а декларативный — ЧТО нужно получить (описание желаемого исхода).
Скорее всего, именно так вы ответите на вопрос, чем отличаются эти стили программирования, и по сути вы будете правы. Но давайте рассмотрим простой пример:
Сможете чётко ответить, какой именно стиль программирования тут используется, ЧТО или КАК? Если вы засомневались или подумали что это императивный стиль, то добро пожаловать в клуб, т.к. это декларативный стиль.
Вместо использования КАК и ЧТО, предлагаю следующее определение:
Императивный стиль характеризуется явным выполнением операций, которые что‑то «делают» во «внешнем мире» (мутируют данные, вызывают системные API и т.п.), тогда как декларативный стиль сводится к написанию чистых выражений и функций, которые лишь вычисляют значения.
Кстати, если вы помните (и понимаете) концепцию функционального мышления, то в целом можно сказать что императивный стиль — «действия», а декларативный — «вычисления».
Императивный стиль описывает КАК достичь результата (пошаговые инструкции), а декларативный — ЧТО нужно получить (описание желаемого исхода).
Скорее всего, именно так вы ответите на вопрос, чем отличаются эти стили программирования, и по сути вы будете правы. Но давайте рассмотрим простой пример:
const isEven = count % 2 === 0;
Сможете чётко ответить, какой именно стиль программирования тут используется, ЧТО или КАК? Если вы засомневались или подумали что это императивный стиль, то добро пожаловать в клуб, т.к. это декларативный стиль.
Вместо использования КАК и ЧТО, предлагаю следующее определение:
Императивный стиль характеризуется явным выполнением операций, которые что‑то «делают» во «внешнем мире» (мутируют данные, вызывают системные API и т.п.), тогда как декларативный стиль сводится к написанию чистых выражений и функций, которые лишь вычисляют значения.
Кстати, если вы помните (и понимаете) концепцию функционального мышления, то в целом можно сказать что императивный стиль — «действия», а декларативный — «вычисления».
👍1👎1🔥1
Простой пример сокрытия императивного кода за декларативным интерфейсом
Для начала вспомним, что декларативное программирование фокусируется на описании желаемого результата, вместо указания пошаговых действий. Основное преимущество этого стиля в том, что он делает код короче и выразительнее, скрывая детали реализации.
Простой пример, где isLoading отвечает за блокировку кнопки Submit:
Императивным тут является только onSubmit, который, помимо основной задачи, контролирует состояние формы через setLoading.
Разделяем ответственность и скрываем часть императивного кода внутри useLoading:
Теперь onSubmit выглядит чище и отвечает только за то, что и должен.
П.С. Для SolidJS useLoading может выглядеть так:
Для начала вспомним, что декларативное программирование фокусируется на описании желаемого результата, вместо указания пошаговых действий. Основное преимущество этого стиля в том, что он делает код короче и выразительнее, скрывая детали реализации.
Простой пример, где isLoading отвечает за блокировку кнопки Submit:
function CreateOrder(props) {
const [isLoading, setLoading] = createSignal(false);
function onSubmit() {
setLoading(true);
props.onSubmit()
.then(() => {/* ... */})
.catch(() => {/* ... */})
.finally(() => setLoading(false));
}
return <Form onSubmit={onSubmit}>...</Form>;
}
Императивным тут является только onSubmit, который, помимо основной задачи, контролирует состояние формы через setLoading.
Разделяем ответственность и скрываем часть императивного кода внутри useLoading:
function CreateOrder(props) {
const [isLoading, submit] = useLoading(props.onSubmit);
function onSubmit() {
submit()
.then(() => {/* ... */})
.catch(() => {/* ... */});
}
return <Form onSubmit={onSubmit}>...</Form>;
}
Теперь onSubmit выглядит чище и отвечает только за то, что и должен.
П.С. Для SolidJS useLoading может выглядеть так:
import { type Accessor, createSignal } from 'solid-js';
export function useLoading<
T,
P extends any[],
A extends ((...args: P) => Promise<T>) | undefined
>(action: A): [Accessor<boolean>, A] {
const [loading, setLoading] = createSignal(false);
const handler = action && (((...args: P): Promise<T> => {
setLoading(true);
return action(...args).finally(() => setLoading(false));
}) as A);
return [loading, handler];
}
Vite vs Webpack: нюансы сборки
В проекте на Vite обнаружил неожиданное (для меня) поведение: при загрузке чанка страницы, браузер также загружает чанки двух других, не связанных с ней, страниц:
Почему так? Vite (rollup) помещает общий компонент, который используется в нескольких асинхронных чанках, в один из них. Другими словами, при загрузке любого из этих чанков, браузер вынужден загрузить и тот чанк, в котором находятся общий компонент.
Однако, это не является поведением по умолчанию. Всему виной параметр experimentalMinChunkSize. Без него, Vite выносит все общие компоненты в отдельные чанки. Даже если компонент весит всего 106 байт, как этот:
В итоге получается, либо много маленьких чанков (поведение по умолчанию), либо браузер загружает много не связанного кода (при использовании параметра).
На заметку: experimentalMinChunkSize работает не всегда и в проекте всё равно остаются микро-чанки.
Webpack работает иначе: помещает копию компонента в каждый асинхронный чанк, где этот компонент необходим, если его размер не превышает значения указанного в splitChunks.minSize (по умолчанию 20000 байт). В противном случае, компонент выносится в отдельный чанк.
В проекте на Vite обнаружил неожиданное (для меня) поведение: при загрузке чанка страницы, браузер также загружает чанки двух других, не связанных с ней, страниц:
./page-a.js
./page-b.js
./page-c.js
Почему так? Vite (rollup) помещает общий компонент, который используется в нескольких асинхронных чанках, в один из них. Другими словами, при загрузке любого из этих чанков, браузер вынужден загрузить и тот чанк, в котором находятся общий компонент.
Однако, это не является поведением по умолчанию. Всему виной параметр experimentalMinChunkSize. Без него, Vite выносит все общие компоненты в отдельные чанки. Даже если компонент весит всего 106 байт, как этот:
import {c as o} from "./createIcon.js";
const c = o("24", "Stroked", "Cancel");
export {c as C};
В итоге получается, либо много маленьких чанков (поведение по умолчанию), либо браузер загружает много не связанного кода (при использовании параметра).
На заметку: experimentalMinChunkSize работает не всегда и в проекте всё равно остаются микро-чанки.
Webpack работает иначе: помещает копию компонента в каждый асинхронный чанк, где этот компонент необходим, если его размер не превышает значения указанного в splitChunks.minSize (по умолчанию 20000 байт). В противном случае, компонент выносится в отдельный чанк.
👍8🤔1
Nullish coalescing assignment (??=)
Интересно, что в отличии от nullish coalescing operator (??), assignment (??=) как будто ни кто не использует.
Интересно, что в отличии от nullish coalescing operator (??), assignment (??=) как будто ни кто не использует.
// Вместо:
if (!date) {
date = Date.now();
}
// Пишем:
date ??= Date.now();
👍7
Vite меняет хеши всех js чанков при любом изменении
Реальный проект на React, собирается Vite, на выходе получается примерно 60 js файлов:
Меняем исходный код в любом месте проекта, собираем заново, и получаем те же самые js файлы, но с другими хешами в именах. В действительности код изменился только в одном файле, но почему же везде поменялись хеши?
Vite использует es6 modules:
Заглядываем в любой другой модуль, и видим следующее:
Все синхронные зависимости, в том числе node_modules, Vite складывает во «входную точку» — index-DGZT2d08.js. В результате получаем следующее поведение:
Файл с реальными изменениями собирается с новым хешем (ожидаемо), это провоцирует изменение хешей всей цепочки «родителей», вплоть до входной точки (тоже, в целом, ожидаемо). Изменение последней провоцирует изменение всех остальных файлов, т.к. все они ссылаются на эту самую входную точку.
Это плохо, т.к. вынуждает браузер пользователя скачивать все необходимые файлы заново, даже если в них, кроме путей импортов и экспортов, ни чего не поменялось.
Реальный проект на React, собирается Vite, на выходе получается примерно 60 js файлов:
index-DGZT2d08.js
page-DMUtS7f0.js
page-CIOhQsD0.js
и т.д.
Меняем исходный код в любом месте проекта, собираем заново, и получаем те же самые js файлы, но с другими хешами в именах. В действительности код изменился только в одном файле, но почему же везде поменялись хеши?
Vite использует es6 modules:
// index-DGZT2d08.js
const Page = lazy(() => (
__vitePreload(() => (
import("./page-DMUtS7f0.js")
))
));
Заглядываем в любой другой модуль, и видим следующее:
// page-DMUtS7f0.js
import { jsx } from "./index-DGZT2d08.js";
Все синхронные зависимости, в том числе node_modules, Vite складывает во «входную точку» — index-DGZT2d08.js. В результате получаем следующее поведение:
Файл с реальными изменениями собирается с новым хешем (ожидаемо), это провоцирует изменение хешей всей цепочки «родителей», вплоть до входной точки (тоже, в целом, ожидаемо). Изменение последней провоцирует изменение всех остальных файлов, т.к. все они ссылаются на эту самую входную точку.
Это плохо, т.к. вынуждает браузер пользователя скачивать все необходимые файлы заново, даже если в них, кроме путей импортов и экспортов, ни чего не поменялось.
👍3🤔3
Why did this render? Props changed!
Это не совсем так! Вот список того, что на самом деле вызывает повторные рендеры в React:
• Изменение локального состояния компонента
• Повторный рендер родительского компонента
• Изменения значения контекста, на который подписан компонент
Это важно понимать: при повторном рендере компонента, также выполнится рендер и всех его потомков, даже если props у них не менялись!
Это не совсем так! Вот список того, что на самом деле вызывает повторные рендеры в React:
• Изменение локального состояния компонента
• Повторный рендер родительского компонента
• Изменения значения контекста, на который подписан компонент
Это важно понимать: при повторном рендере компонента, также выполнится рендер и всех его потомков, даже если props у них не менялись!
❤3
React: memo и useMemo
Это инструменты, решающие конкретные задачи, или головная боль разработчиков?
Прежде чем отвечать на этот вопрос, рассмотрим три примера:
1️⃣ Контекст
При каждом рендере провайдер получает новый объект. При этом, подписанные на этот контекст компоненты также будут выполнены. Как это выглядит:
Обернём данные для контекста в useMemo:
Экономия ~330 ms.
2️⃣ Список из 100 карточек
Результаты первого и второго рендера:
Обернём Item в React.memo:
Во втором рендере получаем экономию ~34 ms.
3️⃣ Поиск по массиву из ~900 объектов
Добавим debounce для поиска и useMemo для list. В итоге, среднее значение рендера на каждый введённый, в поисковую строку, символ будет:
При этом, с debounce рендеров будет больше, а формирование list в обоих случаях занимает ~2ms.
*️⃣ Выводы
Однозначного ответа когда, где и как именно их нужно применять нет, в отличие от того же createMemo из SolidJS. На примерах также видно, что какого-то прям заметного профита, который будет стоить потрачено времени и сил, тоже нет.
В общем, если использовать их точечно, когда реально что-то тормозит и это заметно, то это похоже на инструменты, в остальных случаях — лишняя когнитивная нагрузка (головная боль).
П.С. Все замеры производились на MacBook Air m1 в Chrome с 4х замедлением CPU и React в dev режиме.
Это инструменты, решающие конкретные задачи, или головная боль разработчиков?
Прежде чем отвечать на этот вопрос, рассмотрим три примера:
1️⃣ Контекст
function Page() {
const { id } = useParams();
const { data } = useQueryData();
// ...fetching other data
return (
<Context.Provider
value={{ id, value: data.value }}
>
<Content />
</Context.Provider>
);
}
При каждом рендере провайдер получает новый объект. При этом, подписанные на этот контекст компоненты также будут выполнены. Как это выглядит:
180ms <= первый рендер
161ms <= данные пришли
107ms <= данные не меняются
156ms
107ms
Обернём данные для контекста в useMemo:
197ms <= первый рендер
160ms <= данные пришли
10ms <= !!!
18ms
9ms
Экономия ~330 ms.
2️⃣ Список из 100 карточек
function Page() {
const [items, showMore] = useSomeList();
return (
<>
<List>
{items.map((item) => (
<Item {...item} />
))}
</List>
<Button onClick={showMore}>
Ещё
</Button>
</>
);
}
Результаты первого и второго рендера:
51ms <= 50 items
97ms <= 100 items
Обернём Item в React.memo:
58ms <= 50 items
63ms <= 100 items
Во втором рендере получаем экономию ~34 ms.
3️⃣ Поиск по массиву из ~900 объектов
function Modal() {
const search = useSearch();
const users = useUsers();
const list = users
.filter(bySearch(search))
.sort(toSomeOrder)
.slice(0, 20);
return (
<>
<Search {...search} />
<UsersList data={list} />
</>
);
}
Добавим debounce для поиска и useMemo для list. В итоге, среднее значение рендера на каждый введённый, в поисковую строку, символ будет:
В исходном варианте: ~22ms
debounce + useMemo: ~20ms
При этом, с debounce рендеров будет больше, а формирование list в обоих случаях занимает ~2ms.
*️⃣ Выводы
Однозначного ответа когда, где и как именно их нужно применять нет, в отличие от того же createMemo из SolidJS. На примерах также видно, что какого-то прям заметного профита, который будет стоить потрачено времени и сил, тоже нет.
В общем, если использовать их точечно, когда реально что-то тормозит и это заметно, то это похоже на инструменты, в остальных случаях — лишняя когнитивная нагрузка (головная боль).
П.С. Все замеры производились на MacBook Air m1 в Chrome с 4х замедлением CPU и React в dev режиме.
👍1
Нужно ли знать алгоритмы и структуры данных?
Вы когда-нибудь задумывались, почему этот вопрос возникает исключительно в контексте оценки одних разработчиков другими, например, на собеседованиях?
В своё время я не знал ни одного языка программирования, ни, тем более, алгоритмов и структур данных. Но это не помешало мне разрабатывать коммерческие продукты, которые выполняли поставленные задачи, и делали это быстро и качественно.
Да, фундаментальные знания, в том числе алгоритмы и структуры данных, важны. И каждый разработчик, на определённом этапе своей карьеры, обязательно с ними познакомится. Однако, наличие этих знаний не гарантирует, что разработчик будет писать хороший код. Верно и обратное.
Не нужно превращать алгоритмы в культ. Хороший разработчик не обязан зубрить LeetCode, чтобы пройти техническое собеседование!
Вы когда-нибудь задумывались, почему этот вопрос возникает исключительно в контексте оценки одних разработчиков другими, например, на собеседованиях?
В своё время я не знал ни одного языка программирования, ни, тем более, алгоритмов и структур данных. Но это не помешало мне разрабатывать коммерческие продукты, которые выполняли поставленные задачи, и делали это быстро и качественно.
Да, фундаментальные знания, в том числе алгоритмы и структуры данных, важны. И каждый разработчик, на определённом этапе своей карьеры, обязательно с ними познакомится. Однако, наличие этих знаний не гарантирует, что разработчик будет писать хороший код. Верно и обратное.
Не нужно превращать алгоритмы в культ. Хороший разработчик не обязан зубрить LeetCode, чтобы пройти техническое собеседование!
👍7💔1
Подключил React Compiler к реальному проекту, и вот что из этого получилось.
Входные данные:
Время сборки выросло в 2 раза:
Размер чанков (с React компонентами) вырос почти в 1.5 раза:
Справедливости ради, в целом проект подрос не так заметно:
Время рендера страниц (обычная сборка => compiler):
Стоит отметить, что на этих страницах отсутствует какая-либо «ручная» мемоизация.
Выводы каждый сделает сам.
П.С. Замеры производились на MacBook Air m1 + Chrome с 4х замедлением CPU.
Входные данные:
vite v6.2
react v18.3
babel-plugin-react-compiler v1.0
react-compiler-runtime v1.0
Время сборки выросло в 2 раза:
plugin-react ~21 s
plugin-react + react-compiler ~40 s
Размер чанков (с React компонентами) вырос почти в 1.5 раза:
chunk-a.js 26.6 => 38.2 Kb
chunk-b.js 30.6 => 38.9 Kb
Справедливости ради, в целом проект подрос не так заметно:
6.1 => 6.8 Mb
Время рендера страниц (обычная сборка => compiler):
page-a 18/72/60 => 20/74/30 ms
page-b 26/128 => 37/126 ms
Стоит отметить, что на этих страницах отсутствует какая-либо «ручная» мемоизация.
Выводы каждый сделает сам.
П.С. Замеры производились на MacBook Air m1 + Chrome с 4х замедлением CPU.
Tree Shaking не работает по умолчанию!
Production сборка большинства проектов включает два основных этапа: сборка исходного кода в JavaScript файлы (чанки), и их последующая минификация. При этом, удаление не используемого кода возможно на обоих этапах. На первом за это отвечает Tree Shaking, а на втором — минификатор.
В зависимости от инструментов и их настроек, результат сборки может существенно отличаться. Именно поэтому в бандле могут оказаться чанки которые никто и никогда не использует, а инструменты анализа бандла могут показывать то, чего в чанках нет.
С настройками по умолчанию, Tree Shaking в Vite работает лишь частично, а в Webpack вообще не работает. Чтобы его включить, необходимо в package.json добавить:
Само собой, добавлять sideEffects нужно через тестирование, особенно на большом проекте, т.к. код может содержать реальные побочные эффекты.
Подробнее можно почитать в документации Webpack.
Для контекста рекомендую ознакомиться со следующими постами:
Vite vs Webpack: нюанс с React.memo
Vite vs Webpack: PURE комментарии
Production сборка большинства проектов включает два основных этапа: сборка исходного кода в JavaScript файлы (чанки), и их последующая минификация. При этом, удаление не используемого кода возможно на обоих этапах. На первом за это отвечает Tree Shaking, а на втором — минификатор.
В зависимости от инструментов и их настроек, результат сборки может существенно отличаться. Именно поэтому в бандле могут оказаться чанки которые никто и никогда не использует, а инструменты анализа бандла могут показывать то, чего в чанках нет.
С настройками по умолчанию, Tree Shaking в Vite работает лишь частично, а в Webpack вообще не работает. Чтобы его включить, необходимо в package.json добавить:
{ "sideEffects": false }
Само собой, добавлять sideEffects нужно через тестирование, особенно на большом проекте, т.к. код может содержать реальные побочные эффекты.
Подробнее можно почитать в документации Webpack.
Для контекста рекомендую ознакомиться со следующими постами:
Vite vs Webpack: нюанс с React.memo
Vite vs Webpack: PURE комментарии
🔥2
Как структурировать код, а не просто раскладывать файлы по папкам
Существует распространенное заблуждение, что правильно выбранная структура папок и файлов поможет сделать хорошую архитектуру. В действительности же, файловая структура это лишь следствие архитектуры самого кода.
Чёткое понимание, из каких компонентов состоит ваш код, и как эти компоненты могут взаимодействовать друг с другом, подскажет вам как именно организовать файловую структуру ваших проектов.
Итак, код любого приложения можно разделить на три главных компонента:
Данные
В оригинале, это «факты, относящиеся к событиям», которые я дополняю структурами данных и системой типов. По сути, это любые данные, которыми оперирует код, а также типы описывающие их.
Вычисления
Компоненты, «преобразующие ввод в вывод», также называемые чистыми функциями. Сюда же относятся и презентационные (глупые) UI компоненты.
Действия
Компоненты, которые «влияют на окружающий мир или находятся под его воздействием», также называемые грязными функциями, или функциями с побочными эффектами. К действиям, в том числе, относятся управляющие (умные) UI компоненты.
Дополнительно, каждый компонент можно разделить на две категории:
Платформа
Включает код для работы с базовыми типами данных (объекты, массивы и т.д.) и API платформы. Сюда же попадают и npm пакеты не привязанные к конкретной предметной области, например react.
Предметная область
Включает код отвечающий за описание структуры предметной области, и содержащий ту самую «бизнес логику».
Резюме
Именно действия являются главной причиной выполнения программ, и они же являются источником большинства ошибок. Также важно понимать, что любой код, использующий действия, сам становится действием!
Действия — зона повышенных рисков!
Для снижения рисков и потенциальных проблем, при проектировании и разработке, необходимо:
— Строго разграничивать код, отвечающий за данные, вычисления и действия;
— Снижать количество кода отвечающего за действия, в пользу вычислений;
— Объединять (логически) связанные действия в одном месте (вспоминаем принцип SRP).
Существует распространенное заблуждение, что правильно выбранная структура папок и файлов поможет сделать хорошую архитектуру. В действительности же, файловая структура это лишь следствие архитектуры самого кода.
Чёткое понимание, из каких компонентов состоит ваш код, и как эти компоненты могут взаимодействовать друг с другом, подскажет вам как именно организовать файловую структуру ваших проектов.
Итак, код любого приложения можно разделить на три главных компонента:
Данные
В оригинале, это «факты, относящиеся к событиям», которые я дополняю структурами данных и системой типов. По сути, это любые данные, которыми оперирует код, а также типы описывающие их.
Вычисления
Компоненты, «преобразующие ввод в вывод», также называемые чистыми функциями. Сюда же относятся и презентационные (глупые) UI компоненты.
Действия
Компоненты, которые «влияют на окружающий мир или находятся под его воздействием», также называемые грязными функциями, или функциями с побочными эффектами. К действиям, в том числе, относятся управляющие (умные) UI компоненты.
Дополнительно, каждый компонент можно разделить на две категории:
Платформа
Включает код для работы с базовыми типами данных (объекты, массивы и т.д.) и API платформы. Сюда же попадают и npm пакеты не привязанные к конкретной предметной области, например react.
Предметная область
Включает код отвечающий за описание структуры предметной области, и содержащий ту самую «бизнес логику».
Резюме
Именно действия являются главной причиной выполнения программ, и они же являются источником большинства ошибок. Также важно понимать, что любой код, использующий действия, сам становится действием!
Действия — зона повышенных рисков!
Для снижения рисков и потенциальных проблем, при проектировании и разработке, необходимо:
— Строго разграничивать код, отвечающий за данные, вычисления и действия;
— Снижать количество кода отвечающего за действия, в пользу вычислений;
— Объединять (логически) связанные действия в одном месте (вспоминаем принцип SRP).
👍2
Vite и importmap
Не так давно я писал, что Vite меняет хеши всех JS чанков при любом изменении. В комментариях резонно спросили, а есть ли способ это исправить. Давайте разбираться.
Недавно Vite переехал на rolldown, у которого, в свою очередь, есть экспериментальная опция chunkImportMap. При включении которой, rolldown создаёт файл importmap.json с картой путей до всех JS файлов бандла, где ключи это пути указанные в импортах, а значения — то, как в действительности называются файлы:
Это именно то, что нужно, т.к. хеши в ключах не меняются. Вследствие чего, исчезает и необходимость менять ссылки из других чанков, и, соответственно, менять их хеши.
Однако, чтобы всё заработало, этого не достаточно.
Начиная с марта 2023 года, всеми основными браузерами поддерживается новый тип тега
И вроде бы всё работает, но всплыл неприятный момент — если в вашем бандле есть асинхронные CSS чанки, то они больше никогда не загрузятся.
Покопавшись в файлах бандла, я обнаружил, что при включенной опции chunkImportMap, CSS файлы исключаются из списка предзагружаемых модулей асинхронных чанков.
Как итог, единственным реально рабочим вариантом остаётся дождаться появления соответствующей опции в Vite v8.1 (если верить соответствующему PR). Ну или перейти на rsbuild т.к. он быстрее, предлагает больше опций и точнее собирает код.
П.С. Интересно, что два года назад уже был похожий PR, но он до сих пор так и висит открытым.
Не так давно я писал, что Vite меняет хеши всех JS чанков при любом изменении. В комментариях резонно спросили, а есть ли способ это исправить. Давайте разбираться.
Недавно Vite переехал на rolldown, у которого, в свою очередь, есть экспериментальная опция chunkImportMap. При включении которой, rolldown создаёт файл importmap.json с картой путей до всех JS файлов бандла, где ключи это пути указанные в импортах, а значения — то, как в действительности называются файлы:
{
"imports": {
"/index-BbVZ37fA.js": "/index-CEAfcI3S.js",
"/admin-V_WO83n5.js": "/admin-BACbm3dr.js",
"/user-BR8BcyAZ.js": "/user-CYtl3HzT.js"
}
}
Это именно то, что нужно, т.к. хеши в ключах не меняются. Вследствие чего, исчезает и необходимость менять ссылки из других чанков, и, соответственно, менять их хеши.
Однако, чтобы всё заработало, этого не достаточно.
Начиная с марта 2023 года, всеми основными браузерами поддерживается новый тип тега
<script type="importmap">, в который и нужно положить карту из importmap.json. Эта задача легко решается через добавление соответствующей строки в index.html и плагина для Vite:
{
name: "inject-import-map",
transformIndexHtml(html, { bundle }) {
const chunkImportMap = bundle["importmap.json"];
if (chunkImportMap?.type !== "asset") return html;
const updated = html.replace(
/<script\s+type="importmap"[^>]*>[\s\S]*?<\/script>/i,
`<script type="importmap">${chunkImportMap.source}</script>`,
);
delete bundle["importmap.json"];
return updated;
},
},
И вроде бы всё работает, но всплыл неприятный момент — если в вашем бандле есть асинхронные CSS чанки, то они больше никогда не загрузятся.
Покопавшись в файлах бандла, я обнаружил, что при включенной опции chunkImportMap, CSS файлы исключаются из списка предзагружаемых модулей асинхронных чанков.
Как итог, единственным реально рабочим вариантом остаётся дождаться появления соответствующей опции в Vite v8.1 (если верить соответствующему PR). Ну или перейти на rsbuild т.к. он быстрее, предлагает больше опций и точнее собирает код.
П.С. Интересно, что два года назад уже был похожий PR, но он до сих пор так и висит открытым.
❤4👍2