Код на React и TypeScript, который работает быстро. Доклад Яндекса
О преждевременной оптимизации
Многие из вас не один раз слышали эту фразу, а некоторые даже сами ее произносили: «Не занимайтесь преждевременной оптимизацией». Фраза родилась уже довольно давно, в то время, когда писали на языках довольно низкого уровня и единственной методикой разработки был waterfall.
Это значило, что чаще всего проект в первый раз собирался только перед сдачей в тестирование и эксплуатацию. Поэтому было очень сложно заранее угадать, какая из тысяч строк кода, какой из сотен модулей потребуют оптимизации. Тогда эта фраза была актуальна.
Но прошли годы и десятилетия. У нас появились новые методологии разработки, появилась концепция MVP. Мы как можно раньше собираем рабочий прототип и отдаем его в тестирование и эксплуатацию. Кроме того, мы можем запускать отдельные компоненты, не дожидаясь всего проекта. Для этого у нас есть и тесты, и Storybook. Но та же самая мантра повторяется до сих пор — не занимайтесь преждевременной оптимизацией, когда бы и на каком бы этапе жизни проекта вы бы ни спросили.
В результате современный фронтенд имеет то, что имеет: самые медленные инструменты сборки, самые тормознутые интерфейсы, самые большие размеры собранных файлов. Для Single Page-приложений гигантские бандлы в мегабайты никого не удивляют. И папка node_modules — одна из самых жирных во всех проектах. Например, у нас на странице поиска она уже превысила три гигабайта и продолжает расти.
О чем же будет мой доклад? В первую очередь, наши языки, TypeScript и JavaScript, и наши библиотеки и фреймворки подразумевают, что практически у каждой задачи есть несколько вариантов решений. Все они правильные, все дают нужный результат, но не все одинаково эффективны.
Видеть эти варианты заранее и выбирать нужные, а не выбирать заведомо плохой, — это не преждевременная оптимизация. Те тривиальные приемы, про которые я расскажу, дают при консистентном использовании до 5% производительности кода. Это по данным реальных проектов, которые переходили со старых стеков на использование React и TypeScript. Первая часть — про React.
React
Лишние ререндеры
Самая большая проблема в React — это лишние ререндеры. Вообще, библиотека React была создана с упором на то, чтобы как можно чаще и безболезненнее ререндерить всё дерево компонентов. При этом сами компоненты должны отсекать лишние ререндеры там, где пропсы у них не изменились.
Для этого разработчиками предусмотрены штатные средства и для функциональных компонентов, и для классовых. Но мы очень часто обманываем React и заставляем его делать ререндеры там, где это не нужно.
Один из наиболее частых антипаттернов — создание новых объектов и массивов в методе рендера. Как вы понимаете, в случаях, когда нам нужны стиль или значение по умолчанию, свежесозданный объект не равен предыдущему — даже если у него внутри полностью такой же набор свойств.
То есть пропсы у вложенных компонентов в таком случае принудительно изменяются, и оптимизации, которые мы рассматривали на предыдущем слайде, не работают. Компонент ререндерится, даже если у него визуально ничего не изменилось.
Второй частый кейс — это обработчики событий, когда мы прямо в атрибутах вложенного компонента генерируем новую стрелочную функцию или новый bind, чтобы привязать обработчик к контексту.
Здесь новая стрелочная функция тоже не равна точно такой же предыдущей, и новый результат bind не равен предыдущему. То есть мы опять обманываем вложенные компоненты и заставляем их ререндериться без нужды.
Многие хуки, такие как useState и useReducer, возвращают из себя какие-то функции. В данном случае setCount. И очень просто на лету сгенерировать стрелочную функцию, использующую setCount, чтобы передать ее во вложенный компонент.
Мы знаем из предыдущего примера, что эта новая функция заставит вложенный компонент перерендериться. Хотя разработчики React и хуков явно говорят в документации, что функции, которые возвращаются из useState и из useReducer, не меняются при ререндерах. То есть вы можете получить самую первую функцию, запомнить ее и не перегенерировать свои функции и пропсы при новых вызовах useState. Это очень важно, это часто забывают.
Если вы пишете свои хуки, тоже обратите на это внимание, чтобы ваши функции, возвращаемые из хуков, удовлетворяли этому же требованию, чтобы можно было запомнить первую функцию и потом ее переиспользовать, не заставляя ререндериться вложенные компоненты.
const Foo = () => (
<Consumer>{({foo, update}) => (...)}</Consumer>
);
const Bar = () => (
<Consumer>{({bar, update}) => (...)}</Consumer>
);
const App = () => (
<Provider value={...}>
<Foo />
<Bar />
</Provider>
);
Про контекст. Предположим, у нас небольшое приложение или жесткое ограничение на размер файлов, которые скачиваются на клиент, и мы не хотим втаскивать тяжелую библиотеку типа Redux или других библиотек для управления состоянием — то есть мы считаем их слишком тяжелыми или медленными. Тогда мы можем использовать контекст, чтобы прокинуть свойства до глубоко вложенных компонентов.
Минимальный пример выглядит примерно так. При этом мы можем захотеть сэкономить и вместо двух разных контекстов завести один, в котором хранятся все нужные нам свойства.
В этом есть две потенциальных проблемы. Первая: внутри Context Provider при изменении контекста может перерендериться все, что в него вложено, то есть непосредственно все, что вложено внутри Provider, — и те компоненты, которые зависят от контекста, и те, которые не зависят. Очень важно, когда вы пишете такие вещи с использованием контекста, сразу же проверить, чтобы такого не было.
Советуют при этом делать так: выносить провайдер контекста в отдельный компонент, внутри которого не будет ничего кроме children, и уже в этот компонент оборачивать компоненты, куда дальше передавать контекст.
Вторая потенциальная проблема: у нас два не связанных между собой свойства в контексте, и при изменении одного из них ререндерятся все Consumer, даже те, которых это изменение не должно касаться.
Разработчиками React и контекста предусмотрен способ, как это предотвратить.
Есть битовые маски. При задании контекста мы указываем функцию, которая указывает в битовой маске, что именно изменилось в контексте. И в конкретном Context Consumer мы можем указать битовую маску, которая будет фильтровать изменения и ререндерить вложенный компонент, только если изменились те биты, которые нам нужны.
Пакет, который называется Why Did You Render, — это однозначный must have для всех, кто борется с лишними ререндерами. Он лежит в NPM, ставится довольно легко и в режиме разработчика позволяет в консоли Developer Tools браузера отследить все компоненты, которые перерендериваются, хотя фактически содержимое props и state у них не изменилось.
Вот пример скриншота. Это тот же антипаттерн, когда мы генерируем на каждый рендер новый объект в атрибуте style. При этом в консоли выведется предупреждение, что props фактически не изменились, а изменились только по ссылке, и вы этого ререндера могли избежать.
Если подвести итог, что у нас есть для борьбы с лишними ререндерами:
- Пакет Why Did You Render. Это must have в любом проекте, у любого разработчика на React.
- В Developer Tools браузера Chrome можно включить опцию Paint flashing. Тогда он будет подсвечивать те области экрана, которые перерисовались. Вы визуально заметите, что и как часто у вас ререндерится.
- Самое убойное средство — это в каждый рендер вставить console.log. Это позволяет оценить, сколько вообще у вас ререндеров: и нужных, и ненужных.
- И еще одна вещь: часто забываемый второй параметр в React.memo. Это функция, которая позволит вручную написать код сравнения props с предыдущими и самому возвращать true/false, то есть дополнительно к сравнению по ссылке сравнивать какое-то содержимое. Функция аналогична методу shouldComponentUpdate для классовых компонентов.
HTML-комментарии
Следующий интересный момент — комментарии в HTML-коде, который сгенерирован на сервере.
ReactDOMServer.renderToString(
<div>{someVar}bar</div>
);
<div data-reactroot="">foo<!-- -->bar</div>
В местах склейки статического текста и текста из JavaScript’овых переменных React вставляет HTML-комментарий. Это сделано, чтобы безболезненно гидрировать такие места на клиенте.
ReactDOMServer.renderToString(
<div>{`${someVar}bar`}</div>
);
<div data-reactroot="">foobar</div>
Если вам нужно удалить такой комментарий, то вы склеиваете строки в JS-коде и вставляете в JSX всю склеенную строку, как в этом примере. Почему это важно?
Представьте, что вы разрабатываете интернет-магазин или список товаров. В строке диапазона цен товара получается целых четыре комментария в местах склейки. Если вы видите на странице список из 100 товаров, то у вас отрендерятся три килобайта HTML-комментариев.
То есть при server-side rendering мы вынуждены потратить лишние ресурсы процессора, лишнюю память и лишнее время на то, чтобы их отрендерить. Мы должны передать на клиент эту лишнюю разметку, а браузер должен эти три килобайта распарсить. И пока страница будет открыта, браузер будет держать их в памяти, потому что они присутствуют в дереве DOM документа.
То есть очень важно в горячих местах понимать, почему и откуда приходят эти комментарии, и при необходимости вырезать их за счет способа, который я показал.
HOC
function withEmptyFc(WrappedComponent) {
return props => <WrappedComponent {...props} />;
}
function withEmptyCc(WrappedComponent) {
class EmptyHoc extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
return EmptyHoc;
}
Про HOC. Сегодня на Я.Субботнике уже рассказывали про него. Пустой минимальный HOC, который не делает ничего, выглядит примерно так. Вот два примера: в функциональном и в классовом стиле.
Если замерить производительность server-side rendering, то пустая кнопка, классическая кнопка HTML, рендерится 0,9 микросекунды. Если мы ее обернем в пустой HOC, который не делает ничего, то увидим, что это уже добавляет замедление в рендеринг.
А если мы в этот HOC добавим еще и полезной нагрузки (приведен пример реального HOC из нашего проекта), то увидим, что скорость рендеринга замедлилась еще больше. Почему так происходит?
При server side rendering и при первом рендеринге на клиенте HOC всегда делает вызов React.createElement. Это довольно сложная функция, которая выполняет довольно много работы внутри самой библиотеки React. Она не может не занимать дополнительного времени.
Также происходит копирование props. Мы снаружи HOC получили какие-то props и должны сформировать новые props для вложенного в HOC компонента. Это тоже занимает время.
При ререндере у нас никуда не делся React.createElement. Также HOC добавляет обертку в дереве. Сравнение с предыдущим деревом и обход дерева замедляет работу с ним.
В итоге на продакшене это может выглядеть как результат угара по HOC. Только половина разметки в дереве — это полезная нагрузка, а оставшаяся половина — это context consumer, context provider и разнообразные HOC.
То есть React работает с деревом, которое стало в два раза больше, чем без HOC. Он не может не тратить дополнительное время на обработку этого дерева.
И еще один важный момент. Если мы напишем слишком сложный HOC, то можем наткнуться на полную замену дерева при ререндере вместо update предыдущего. Расскажу про это немножко подробнее.
switch (workInProgress.tag) {
case IndeterminateComponent: {
// …
return mountIndeterminateComponent(…);
}
case FunctionComponent: {
// …
return updateFunctionComponent(…);
}
case ClassComponent: {
// …
return updateClassComponent(…);
}
При ререндере React смотрит: если у нас функциональный компонент или классовый, он производит update, то есть берет новое и старое дерево, сравнивает их, находит между ними минимальный дифф и только эти изменения внедряет в старое дерево.
Но если код получается слишком сложный, то React не понимает, что мы от него хотим, и просто монтирует новое дерево взамен старого. Это можно заметить, если у вас есть компоненты, которые при монтировании выполняют какую-то работу — запросы на бэкенд, генерирование uids и т. п. Так что следите за этим.
Если у вас есть выбор, где реализовать вашу функциональность, в HOC или в хуке, то однозначно рекомендую хук. Он по размеру кода меньше, он — всего лишь вызов функции, которая не несет смысла внутри библиотеки React, в то время как в HOC, я уже говорил, React.createElement — сложная вещь. И в HOC добавляются уровни вложенности и прочая ненужная работа. Если можно ее избежать — избегайте.
Изоморфный код
Про изоморфный код. Евангелисты изоморфизма не очень любят углубляться в детали того, как же их изоморфный код работает на наших серверах и наших клиентах. Проблема в том, что мы контролируем наш бэкенд, можем на нем доставить свежую Node.js, которая понимает последний диалект ECMAScript. В то же время на клиенте до сих пор значительная доля древних браузеров, например Internet Explorer 11 или старые Android: четвертый и немножко новее. Поэтому клиентам до сих пор все равно очень часто нужно отдавать ES5.
Поэтому никакими полифилами вы не сможете добавить на клиент понимание нового синтаксиса: стрелочных функций, классов, async await и прочих вещей.
Таким образом, изоморфный код, который нам подсовывают из разных библиотек или который получается с настройками системы сборки по умолчанию, просто не использует все возможности нашей версии Node.js. Это не очень хорошо.
Мы бы хотели, когда пишем изоморфный код на TypeScript, так настроить сборку, чтобы наш TypeScript компилировался в максимально свежий диалект для Node.js. Чтобы именно этот скомпилированный код исполнялся на Node.js при server side rendering. И чтобы для браузеров TypeScript компилировался в подходящий диалект, ES5 или чуть более новый, если вы собираете разные версии кода для старых и новых браузеров.
Если же мы пишем сразу на ECMAScript, то можем нативно писать для Node.js, и в этом случае бонусом будет то, что нам не нужны никакие системы сборки и бандлинга. Мы сразу пишем код, который нативно понимается Node.js. Node.js умеет использовать модульные системы: CommonJS через require, ESM через import. И нам надо только скомпилировать в ES5 для браузеров и собрать в бандлы.
К сожалению, когда рассматривается изоморфный код, об этом часто забывают. В одном из примеров изоморфного кода, который я видел, код вообще компилировался в ES3, и потребители изоморфного кода из этой библиотеки вынуждены были это терпеть на сервере, сжать зубы и выполнять древний код со всеми полифилами для того, что и так уже было в Node.js.
TypeScript
Дизайн языка
Мы плавно перешли к TypeScript. Сначала очень важно упомянуть про дизайн языка. Агрессивная оптимизация производительности скомпилированных программ и система типов, которая позволяет на этапе компиляции доказать, что ваша программа корректна, — это все не является целями дизайна TypeScript. Не является приоритетом при его дальнейшем развитии.
Это официально написано в документации TypeScript. Так что, пожалуйста, оставьте надежды, что вы сможете так сильно типизировать ваш код, что он будет за вас проверять правильность вашей программы. И не надейтесь, что компилятор сделает за вас всю грязную работу и напишет оптимальный код в результате компиляции.
… Spread operator
О чем я хотел бы сказать в первую очередь, это оператор Spread.
Он очень часто используется в коде на React. Но то, что его легко написать, не означает, что его так же легко выполнять.
Потому что при компиляции такого кода TypeScript запишет в модуль на ES5, во-первых, реализацию метода __assign, а во-вторых, его вызов. То есть фактически воткнет полифил для Object.assign.
И только при компиляции в более новые диалекты он будет использовать сам Object.assign. Проблема в том, что если вы пишете на TypeScript библиотеки и компилируете их — проверьте, чтобы в каждом скомпилированном модуле не было понатыкано реализаций этих __assign. То есть не заставляйте потребителя с каждым модулем снова и снова получать код реализации __assign. Для этого есть соответствующие настройки компиляции TypeScript.
И еще одна проблема: Object.assign, если вы знаете, означает клонирование объекта. Клонирование объекта выполняется не за константное время. Чем сложнее объект, чем больше в нем полей, тем больше времени будет занимать клонирование. И с этим связан такой пример фейла.
Это код, который успешно прошел код-ревью, и оказался в продакшене. Казалось бы, здесь ничего сложного нет, все должно красиво работать.
Проблема в том, что на каждой итерации мы выполняем клонирование предыдущего объекта. И соответственно, на N+1 итерации мы вынуждены будем склонировать объект, в котором уже N полей.
Те, кто разбирается в алгоритмах, понимают, что сложность этого алгоритма — O(N2). То есть чем больше исходный массив, тем с квадратичной зависимостью медленнее будет работать такой простенький код. Легко написать, сложно выполнить, как я уже говорил.
Бывает еще вот такой фейл при использовании spread с массивами.
Здесь вы сразу заметили квадратичную сложность вместо линейной. И здесь легче увидеть еще одну проблему: при каждой итерации мы выделяем память для нового массива, потом копируем в него содержимое старого массива и освобождаем его память.
А если массив начинает занимать гигабайт? Представьте: во-первых, постоянно занято 3 ГБ одновременно (1 ГБ — исходный массив, 1 ГБ — предыдущая копия и 1 ГБ — следующая). Во-вторых, на каждой итерации мы копируем из предыдущего расположения массива в следующее 1 ГБ плюс 1 элемент, 1 ГБ плюс 2 элемента и т. д. Ваша задача — заметить такое на код-ревью и не пустить в продакшен.
Также надо заметить, что порядок расположения spread и остальных полей объекта влияет на то, какой код будет сгенерирован. Например, при таком расположении будет сгенерирован один вызов assign.
// TS:
res = {...obj, a: 1};
// компилируется в ES5:
res = __assign(__assign({}, obj), {a: 1});
// хотелось бы:
res = __assign({}, obj, {a: 1});
// или
res = __assign({}, obj);
res.a = 1;
Если же порядок поменяется, это будет означать уже два вложенных вызова assign. Хотя мы хотели бы один вызов или вообще запись поля “a” в объект результата. Почему так происходит? Напоминаю, что генерация оптимального кода — не цель написания и развития языка TypeScript. Он просто обязан учитывать гипотетические крайние случаи: например, когда в объекте есть getter и поэтому он строит универсальный код, который в любых случаях работает правильно, но медленно.
Справедливости ради нужно сказать, что в TSX оптимально компилируется похожий случай, когда есть два объекта props и вы передаете их в компонент таким образом. Здесь будет всего один вызов assign и компилятор понимает, что надо делать эффективно.
… Rest operator
Двоюродный родственник Spread-оператора — это Rest. Те же три точечки, но по-другому.
У нас в коде это чаще всего используется в деструктурировании. Вот один из примеров. Здесь под капотом, чтобы получить объект otherProps, надо выполнить следующую нетривиальную работу: из объекта props скопировать в новый объект otherProps все поля, название которых не равно “prop1”, “prop2” или “prop3”.
Чувствуете, к чему я клоню? При компиляции в ES5 получается примерно такой код:
var blackList = ['prop1', 'prop2', 'prop3'];
var otherProps = {};
// Цикл по всем полям
for (var p in props)
if (
hasOwnProperty(props, p) &&
// Вложенный цикл — поиск в массиве indexOf(p)
blackList.indexOf(p) < 0
)
otherProps[p] = props[p];
Мы итерируемся по всем полям исходного объекта и внутри выполняем поиск каждого объекта по массиву, который происходит за время, зависящее от размера массива blackList. То есть мы можем получить квадратичную сложность на, казалось бы, простой операции деструктурирования. Чем сложнее деструктурирование, чем больше полей в нем упоминается, тем медленнее оно будет работать, с квадратичной зависимостью.
Нативная поддержка Rest в новых Node.js и новых браузерах не спасает. Вот пример бенчмарка (к сожалению, сейчас сайт jsperf.com лежит), который показывает, что даже примитивная реализация Rest с помощью вспомогательных функций чаще всего работает не медленнее, а даже быстрее нативного кода, который сейчас реализован в Node.js и браузерах.
Второй вариант использования Rest — в аргументах. Мы хотим красиво описать аргументы, дать им имена и типы. Но бывает так, что потом мы их собираем и передаем в следующую функцию без изменения, в таком же порядке.
// хотелось бы ES5:
Component.prototype.fn1 = function(path) {
utils.fn2.apply(utils, arguments);
};
Мы бы хотели, чтобы TypeScript понимал такие кейсы и генерировал вызов apply, передавая в него arguments.
// получаем замедление в ES5:
Component.prototype.fn1 = function(path) {
var vars = [];
for (var _i = 1; _i < arguments.length; _i++) {
vars[_i - 1] = arguments[_i];
}
utils.fn2.apply(utils, __spreadArrays([path], vars));
};
Но опять же, TypeScript действует максимально надежно и медленно. Он копирует часть аргументов в промежуточный массив. Потом создает еще один массив из первого аргумента и сливает их в один новый массив, делая кучу ненужной работы и замедляя ваш код.
Если вы пишете библиотеки, смотрите на скомпилированный код внимательно. Такие случаи желательно расписать руками максимально эффективно, вместо того чтобы надеяться на компилятор TypeScript.
=> вместо bind
В относительно свежих диалектах языка появилась интересная фича — стрелочная функция в методах классов. Выглядит это примерно так.
Мы можем описать метод стрелочной функцией, и у него автоматически будет привязан контекст каждого экземпляра при вызове. То есть нам не надо явно вызвать bind. Казалось бы, это хорошо. На самом деле это тянет за собой очень много минусов.
Под капотом такая конструкция означает вот что: в конструкторе объекта создается поле onClick, где записывается стрелочная функция, привязанная к контексту. То есть в прототипе метод onClick не существует!
- Самый очевидный минус: каждый конструктор тратит время на создание этой новой функции.
- Ее код не шарится между экземплярами. Он существует в стольких же экземплярах, сколько у вас создано экземпляров MyComponent.
- Вместо N вызовов одной функции вы получаете по одному вызову N функций в каждом из независимых экземпляров. То есть оптимизатор на такую функцию внимания не обращает, не хочет ее инлайнить или оптимизировать. Она выполняется медленно.
Это только минусы в производительности. Но я еще не закончил.
С наследованием такого кода появляются проблемы:
- Если в классе-потомке мы создадим метод onClick, он будет затерт в конструкторе предка.
- Если мы все-таки как-то создадим метод, то все равно не сможем вызвать super.onClick, потому что на прототипе метода не существует.
- Хоть как-то переопределить onClick в классе-потомке, опять же, можно только через стрелочную функцию.
Это еще не все минусы.
Так как в прототипе метод не существует, то писать тесты на него, использовать mock и spy невозможно. Надо вручную ловить создание конкретного экземпляра, и только на конкретном экземпляре можно будет как-то шпионить за этим методом.
Не используйте стрелочные функции для методов. Это единственный совет, который можно дать.
@boundMethod вместо bind
Хорошо, тогда разработчики говорят: у нас есть декораторы. В частности, такой интересный декоратор @boundMethod, который вместо нас магически привязывает контекст к нашему методу.
import {boundMethod} from 'autobind-decorator';
class Component {
@boundMethod
method(): number {
return this.value;
}
}
Выглядит красиво, но под капотом этот декоратор делает следующие вещи:
const boundFn = fn.bind(this);
Object.defineProperty(this, key, {
get() {
return boundFn;
},
set(value) {
fn = value;
delete this[key];
}
});
Он все равно вызывает bind. И в придачу определяет getter и setter с именем вашего метода. Можно сразу сказать, что getter и setter никогда не работали быстрее, чем обычное чтение и запись поля.
Плюс здесь есть setter, который выполняет подозрительную работу. Плюс все равно вызывается bind. То есть это по производительности никак не лучше, не быстрее, чем если мы просто напишем bind. Это уже не хочется использовать там, где важна скорость работы кода.
class Base extends Component {
@boundMethod
method() {}
}
class Child extends Base {
method = debounce(super.method, 100);
}
Кроме того, очень легко выстрелить себе в ногу и организовать утечку памяти, всего лишь вызвав в классе-потомке debounce для нашего метода.
В DevTools это выглядит примерно так. Мы видим, что в памяти накапливаются старые экземпляры компонента Child. И если посмотреть в одном экземпляре, как у него выглядит этот метод, то мы увидим целую цепочку из bind-function-debounced-bind-function-debounced-… и так далее. И в каждом из этих debounced в замыканиях содержатся предыдущие экземпляры Child. Вот вам утечка памяти на ровном месте, когда можно было ее избежать.
Задним числом хотелось бы сказать: перед тем, как вы решили использовать эту библиотеку в продакшене, хотелось бы посмотреть на то, как работает ее код. Одного знания, что ее код вместо одного вызова bind делает такие вещи, как getter и setter, было бы достаточно, чтобы не хотеть ее использовать.
Мы хотели бы посмотреть на коммиты: как часто они делаются, когда был последний коммит. Хотели бы посмотреть на тесты, насколько вменяемо они написаны. И проанализировать открытые баги — насколько оперативно они исправляются. Этот баг с утечкой памяти, к сожалению, существует до сих пор. Ему уже два года, он скоро пойдет в детский садик, и до сих пор автор не торопится его исправлять.
Не используйте этот декоратор как минимум до тех пор, пока баг не будет исправлен.
TL;DR
Мой рассказ подходит к концу. Вот что я хотел бы еще раз для вас повторить:
- Если вы заранее думаете над кодом и не выбираете заведомо неудачные варианты, которые работают плохо и медленно, это не преждевременная оптимизация. Это, наоборот, хорошо.
- Когда вы делаете осознанный выбор, «скорость или красота кода», — это тоже хорошо, если ваш выбор осознан.
- Очень плохо, если вы в принципе не умеете делать выбор, потому что не видите разных вариантов решения или не знаете, как писать производительный код, или не понимаете разницу в скорости работы вашего кода.
У меня все. Вот ссылка на документ со всеми упомянутыми материалами.