Философия программирования
192 subscribers
17 photos
33 links
Frontend без воды: говорим о технологиях, архитектуре, принципах, кодстайле и том, как превращать хаос в систему.
Download Telegram
Пара нюансов в вопросе Webpack vs Vite

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

1. Обработка ошибок загрузки (не существующих) чанков

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


Для JS: error.name = ChunkLoadError
Для CSS: error.code = CSS_CHUNK_LOAD_FAILED


Vite же, для js вернёт обычную ошибку, сообщение в которой может варьироваться в зависимости от браузера, а для css — заготовленную ошибку Unable to preload CSS.

Про обработку таких ошибок расскажу как нибудь в другой раз.

2. Ручное именование чанков

Иногда возникает потребность в более детальной настройке того, что и как попадёт в конкретный чанк. Для этого, в webpack, есть так называемые магические комментарии https://webpack.js.org/api/module-methods/#magic-comments. В данный момент нас интересует webpackChunkName, который позволяет указать название чанка (то, как будет именоваться js файл).

Естественно, подобные комментарии не работают в Vite. Но, мы не первые, кто столкнулся с этим вопросом, поэтому уже есть плагин https://www.npmjs.com/package/vite-plugin-webpackchunkname. Лично пока не пробовал, могут быть нюансы.

#development #frontend #javascript #webpack #vite
Экспериментальная оптимизация Webpack

Как сократить время локального запуска проекта, например, с 16 до 1 секунды? Если вы используете Webpack 5.17+, можно отложить сборку конкретного кода (чанка) до момента пока он не понадобится:

experiments: {
// true должно быть только при локальном запуске
lazyCompilation: true,
},

В итоге, чем лучше код разделён на чанки, тем меньше времени придётся ждать.

#development #frontend #webpack #optimisation #javascript

https://webpack.js.org/configuration/experiments/#experimentslazycompilation
Как в Webstorm найти не используемый код?

Заходим в меню Code > Analyze Code > Run Inspection By Name, в появившемся поле вводим “unused” и из списка подсказок выбираем Unused global symbol. Ну а далее настраиваем параметры и нажимаем ОК.

#development #frontend #javascript #webstorm
Про код-стайл, зачем он нужен и как должен выглядеть

Код-стайл (он же code style, coding standards, coding convention или programming style) — некоторый набор правил и соглашений для написания кода, над которым работает более одного разработчика.

Зачем нужны эти правила?

Первое, и самое важное — убрать разногласия между разработчиками, т.к. каждый привык писать по своему и считает что именно его вариант самый верный.
Второе — предотвратить появление распространённых ошибок в коде. Например then() написали, а catch() забыли и в итоге у нас ошибка на проде.
Третье — упростить чтение кодовой базы.

В каком виде эти правила и соглашения должны быть реализованы?

Самый эффективный способ — настроить автоматические инструменты для максимального количества правил. При этом, что бы все стилистические правила (табы vs пробелы, длинна строки и т.п.) применялись автоматически, например на pre-commit hook! Почему именно так? Да всё просто, всё что делается автоматом, не должно мешать во время написания кода. Важно отметить, что автоматическое применение стилей не должно происходить до момента пока код не дописан, т.к. есть правила которые удаляют не используемый код.

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

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

Самые популярные инструменты, на данный момент:

https://eslint.org
https://prettier.io

#codestyle #eslint #prettier #frontend #development #javascript
Про чанки

Начать нужно с того, что весь (frontend) код который мы пишем, в конечном итоге загружается пользователями. Помимо самой загрузки, браузеру нужно этот код прочитать, проанализировать и выполнить. На всё это, как ни странно, нужно время, и чем больше этого кода, тем больше времени будут занимать все эти процессы.

Стоит сделать важное замечание: даже если нужно выполнить какую-то небольшую часть кода, загрузить и проанализировать браузер его должен полностью.

Предположим у нас есть веб-приложение с 10 страницами, из которых пользователи используют обычно 2-3, а к остальным обращаются редко, а то и вообще никогда. Будет странно загружать весь код (для всех страниц) одним куском (файлом).

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

Разделение кода — не (совсем) автоматический процесс и требует осознанного подхода. Обычно, в отдельный чанк выносится та часть кода (со всеми её зависимостями), которая была использована через динамический импорт:


import('./something')


Но то, как именно код будет разделён, в конечном счёте зависит от настроек инструментов сборки, которые вы используете.

Как именно делить код, вопрос индивидуальный и зависит от конкретной задачи. Однако можно выделить пару универсальных советов. Во-первых, старайтесь выносить каждую страницу в отдельный чанк. Во-вторых, выносите функционал который не доступен на странице по умолчанию, без дополнительных действий пользователя.

Кстати, если вы пишите приложение с использованием React, то встроенная функция lazy и компонент Suspense должны быть вам хорошо знакомы.

#frontend #development #codesplitting #javascript #react
Паттерн switch(true)

Интересный паттерн который однако встречается достаточно редко. Настолько, что его полноценная поддержка в typescript появилась только в версии 5.3 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#switch-true-narrowing

Если в коде видим большое количество else if условий, например:


if (/* condition #1 */) {
// code #1
} else if (/* condition #2 */) {
// code #2
} else if (/* condition #3 */) {
// code #3
} else {
// code #4
}


Возможно, это то самое место, где можно и нужно применить switch(true):


switch(true) {
case /* condition #1 */:
// code #1
break;
case /* condition #2 */:
// code #2
break;
case /* condition #3 */:
// code #3
break;
default:
// code #4
}


Конечно это субъективно, но на мой взгляд, так выглядит понятнее и самое главное логичнее.

Кстати, в SolidJS есть компоненты которые используют этот паттерн:


<Switch fallback={<div>Not Found</div>}>
<Match when={state.route === "home"}>
<Home />
</Match>
<Match when={state.route === "settings"}>
<Settings />
</Match>
</Switch>


#javascript #typescript #frontend #development #patterns #react #solidjs
Типичная ошибка использования .sort()

Даже опытные разработчики могут не увидеть ошибку в следующем коде:


function sortItems<T>(items: T[]): T[] {
return items.sort(/* some sort function */);
}


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


function sortItems<T>(items: T[]): T[] {
return [...items].sort(/* some sort function */);
}


Видимо это всех настолько “утомило”, что в спецификацию добавили метод toSorted.

Ещё одним “решением” для предотвращения появления этой ошибки в typescript является использование ReadonlyArray. Но, к сожалению, про него практически ни кто не знает.

#frontend #development #javascript #typescript
Пример улучшения функции сортировки

Сортировка — довольно распространённый вид операции с данными в JavaScript / TypeScript. Например, в одном из рабочих проектов .sort встречается 97 раз в 65 файлах. При этом важно, чтобы этот код был максимально читаемым и компактным.

Обратите внимание, что некоторые примеры, для упрощения восприятия, будут без типизации. В конце будет ссылка на рабочий TypeScript код целиком.

Рассмотрим следующий пример:


const MAP_TYPE_TO_ORDER: Record<FeatureType, number> = {
[FeatureType.Default]: 0,
[FeatureType.Local]: 1,
[FeatureType.Unknown]: 2,
};

function sortFeaturesByTypeAndTitle(features: Feature[]): Feature[] {
return [...features].sort((a, b) => {
const numA = MAP_TYPE_TO_ORDER[a.type];
const numB = MAP_TYPE_TO_ORDER[b.type];
if (numA !== numB) {
return numA - numB;
}
return a.title.localeCompare(b.title);
});
}


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

Для сортировки по алфавиту:


function inAbcOrderBy(prop) {
return (a, b) => a[prop].localeCompare(b[prop]);
}


Для сортировки по полю с перечислением (в нашем примере это type) в заданном порядке в виде массива (почему именно массив, объясню в конце):


function inCustomOrderBy(prop, order) {
return (a, b) => order.indexOf(a[prop]) - order.indexOf(b[prop]);
}


Для объединения нескольких функций сортировки в один метод .sort() напишем ещё одну вспомогательную функцию:


function inPriority(...sorters) {
return (a, b) => {
for (let i = 0; i < sorters.length; i++) {
const result = sorters[i](a, b);
if (result !== 0) return result;
}
return 0;
};
}


Осталось переписать наш пример используя эти функции:


const TYPE_ORDER = [
FeatureType.Default,
FeatureType.Local,
FeatureType.Unknown,
];

function sortFeaturesByTypeAndTitle(features: Feature[]): Feature[] {
return [...features].sort(
inPriority(
inCustomOrderBy('type', TYPE_ORDER),
inAbcOrderBy('title')
)
);
}


В итоге мы получили:

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

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

Код полностью: https://gist.github.com/SanichKotikov/c8c3dac1f0de4b722b13c4af49b29c61

#frontend #development #javascript #typescript #sort
https://kettanaito.com/blog/dont-sleep-on-abort-controller

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

Самым вкусным, на мой взгляд, является отмена пачки подписок на события одной командой:


useEffect(() => {
const controller = new AbortController();
const options = { signal: controller.signal };

el.addEventListener('pointerdown', handler, options);
el.addEventListener('pointerup', handler, options);
el.addEventListener('pointercancel', handler, options);
el.addEventListener('pointerleave', handler, options);
el.addEventListener('click', handler, options);

return () => {
controller.abort();
};
}, []);


#frontend #development #javascript #react
👍3
В продолжении темы оптимизации

В прошлом посте я рассказывал про оптимизацию средствами CSS, на примере конкретного свойства. Пришло время поговорить и про JS.

Возьмём всё тот же код с использованием SolidJS: https://github.com/SanichKotikov/solid-emoji-demo

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

В качестве отправной точки возьмём код, в котором стили объявлены через свойство style из JS:


Script ~54 ms
Recalculate: ~7 ms
Layout: ~19 ms
PrePaint: ~2 ms
Paint: ~7 ms


Выносим всю стилизацию, кроме contain-intrinsic-height, из JS в CSS файл, и сразу получаем следующий результат:


Script ~24 ms
Recalculate: ~3 ms
Layout: ~19 ms
PrePaint: ~1.3 ms
Paint: ~6.3 ms


Выполнение JS кода, а также recalculate style, сократились примерно в двое. Кстати, если использовать emotion/css, то это практически ни как не повлияет на изначальные цифры. Делаем очевидный вывод — стилизация через JS (в рантайме) имеет свои накладные расходы.

Интересно, что если унести и contain-intrinsic-height в CSS, само собой с каким-то усреднённым значением, то это может замедлить Layout до ~24 ms.

Как ещё можно сократить время выполнения JS в таком, казалось бы, простом коде? Избавимся от “конструкций” библиотеки (в нашем случае SolidJS), там где это возможно.

Во-первых, избавимся от отдельного компонента EmojiButton, т.к. в нём нет ни какого практического смысла. Запомните, чем больше в проекте компонентов, тем он медленнее, в том числе и на этапе сборки.

Во-вторых, заменим For на обычный .map, т.к. список emoji статический, и в этом компоненте тут нет какого-то особого смысла. Попутно можно удалить и createMemo, по той же причине. Кстати, компонент Index медленнее чем For.

В итоге, получаем ещё немного прироста в выполнении JS:


Script ~19 ms


П.С. Изменения можно посмотреть в том же репозитории, ветки css и js.

#development #performance #solidjs #javascript
Children в SolidJS и порядок выполнения

Недавно столкнулся с не совсем очевидным моментом, связанным с props.children в SolidJS. Для понимания проблемы возьмём вот такой код:


function Wrapper(props) {
return props.when
? props.wrap(props.children, props.when)
: props.children;
}

function App() {
return (
<Wrapper
when={true}
wrap={(content) => (
<Provider>{content}</Provider>
)}
>
<Content />
</Wrapper>
);
}


Примечание: Опустим момент, что компонент Wrapper не реактивный, сейчас нас это не интересует.

Вопреки ожиданиям, компоненты выполнятся в следующем порядке: App > Wrapper > Content > Provider. В результате чего, контекст из Provider будет недоступен в компоненте Content. Чтобы понять, что пошло не так, давайте посмотрим во что превращается JSX при сборке проекта:


function App() {
return _$jsx(Wrapper, {
when: true,
wrap: content => _$jsx(Provider, {
children: content
}),
get children() {
return _$jsx(Content, {});
}
});
}


Параметр children превратился в getter функцию, которая будет выполнена при любой попытке чтения! Например, при передаче в качестве аргумента в функцию:


props.wrap(props.children, props.when)


Очевидным решением проблемы, в данном случае, будет замена интерфейса параметра children компонента Wrapper, с JSX на функцию, вызов которой вернёт этот самый JSX:


function App() {
return (
<Wrapper
when={true}
wrap={(content) => (
<Provider>{content()}</Provider>
)}
>
{() => <Content />}
</Wrapper>
);
}


#development #frontend #solidjs #javascript #jsx
🔥2
React != SolidJS на примере эффектов

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

П.С. Менять состояние из эффектов — довольно плохая идея и лучше такого кода избегать, в React так точно.

#development #frontend #solidjs #react #javascript
🔥1
Причина закрытия WebSocket соединения

Если вы работали с WebSocket без каких либо обёрток, возможно вы обратили внимание, что метод close может принимать два параметра: код и причину закрытия. А CloseEvent, в свою очередь, оба эти значения может содержать.


const socket = new WebSocket(url);

socket.onclose = (event) => {
console.log(event.reason);
};

socket.close(1000, 'Reason');


Однако, этот код не гарантирует что в логе будет 'Reason'! А всё потому, что при вызове метода close, сначала отправляется соответствующая команда на сервер, который кстати может и не ответить. Но даже если он ответил, нет гарантий что он вернёт нам эту же причину.

В общем, CloseEvent.reason — причина закрытия с сервера, поэтому там может быть всё что угодно.

П.С. Есть ещё CloseEvent.wasClean, по которому можно понять было ли соединение закрыто “чисто” или что-то пошло не так.

#development #frontend #javascript
👍2👎1
Принудительное выполнение компонента в SolidJS

Помним, что компоненты в SolidJS, в отличие от React, выполняются только “один” раз. Однако, бывают случаи когда необходимо выполнить компонент “заново” при изменении каких-то данных. Например, нужно полностью обновить форму редактирования при выборе другого блюда:


<Resource data={dishData} onReload={dishActions.refetch}>
{(dish) => (
<Show keyed when={dish().id}>
<DishForm
dish={dish()}
categories={categories()}
onSubmit={onDishSave}
/>
</Show>
)}
</Resource>


Параметр keyed компонента Show как раз для этого и предназначен. При этом, в параметре when необходимо указать значение, при изменении которого произойдёт повторное выполнение.

#development #frontend #javascript #solidjs
🔥1