В некоторых, но очень важных сценариях, которые легко упустить из виду, может возникнуть множество проблем, серьезно влияющих на производительность, но легко решаемых.
Эта статья была впервые опубликована 5 декабря 2017 г. (Оригинальная ссылка), который в основном представляет процесс оптимизации производительности одной из самых посещаемых страниц на веб-странице Airbnb, страницу сведений о листинге, а также методы, инструменты и опыт, используемые в нем.
автор:Joe Lencioni, инженер веб-инфраструктуры Airbnb
Переводчик: Иван Чжун, инженер-стажер Airbnb в Китае
Вычитка: Лоуренс Лин, инженер полного стека Airbnb China
Мы используемReact Routerа такжеHypernovaРазрабатывайте одностраничные приложения, поддерживающие рендеринг на стороне сервера. Первый сценарий примененияairbnb.comосновной процесс бронирования. В начале этого года (примечание: эта статья изначально была написана в 2017 году) мы завершили миграцию главной страницы и страницы результатов поиска с отличными результатами. Следующий план — добавить страницу сведений о листинге в одностраничное приложение.
Это страница сведений о нашем листинге. В процессе поиска пользователи могут посещать эту страницу несколько раз, просматривая разные списки. эта страницаairbnb.comОдна из самых посещаемых и важных страниц в Интернете, поэтому мы хотели выяснить все детали, влияющие на производительность!
Каждая страница неизбежно имеет какое-то взаимодействие, такое как прокрутка, нажатие, ввод текста. В рамках миграции одностраничного приложения я хотел устранить любые проблемы с производительностью, вызванные взаимодействиями на странице сведений о листинге. Мы хотим, чтобы страница запускалась быстро и работала плавно, предоставляя пользователям лучший опыт.
Благодаря процессу анализа, исправления и повторного анализа интерактивная производительность этой ключевой страницы была значительно улучшена, что может сделать процесс бронирования более удобным для пользователей.В этом посте вы узнаете о методах, которые я использовал для анализа этой страницы, инструментах, которые я использовал для ее оптимизации, и увидите влияние изменений на графике пламени.
метод
Анализ страницы был записан с помощью инструментов производительности Chrome:
- Откройте окно в режиме инкогнито (чтобы плагины моего браузера не мешали анализу)
- Посетите страницу, которую вы хотите проанализировать в вашей локальной среде разработки, и используйте ?react_perf в поле запроса (чтобы включить аннотации времени пользователя React), отключив при этом некоторые функции, предназначенные только для разработчиков, которые замедляют работу страницы, напримерaxe-core)
- Нажмите кнопку записи ⚫️
- взаимодействовать со страницей (например, прокручивать, щелкать, печатать)
- Нажмите кнопку записи еще раз 🔴 и проанализируйте результат
Как правило, я выступаю за профилирование на мобильном оборудовании, таком как Moto C Plus, или установку ограничения ЦП на 6-кратное замедление, чтобы понять работу на более медленных устройствах. Однако, поскольку проблемы с производительностью на этой странице достаточно серьезны, на моем очень высококлассном ноутбуке проблемы с производительностью отчетливо видны даже без настройки дроссельной заслонки.
первоначальный рендер
Когда я начал оптимизировать эту страницу, я заметил предупреждение в консоли: 💀
webpack-internal:///36:36 Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
(client) ut-placeholder-label screen-reader-only"
(server) ut-placeholder-label" data-reactid="628"
Это сообщение об ошибке вызвано несоответствием между рендерингом на стороне сервера и рендерингом на стороне клиента. Проблема заставляет веб-браузер выполнять работу, которую он не должен делать с рендерингом на стороне сервера, поэтому всякий раз, когда это происходит, React будет дать такое предупреждение ✋.
К сожалению, сообщение об ошибке не дает четкого представления о том, где именно возникла проблема или что может быть ее причиной, но оно дает нам некоторые подсказки. 🔎Я заметил текст, похожий на классы CSS, поэтому набрал в терминале:
~/airbnb ❯❯❯ ag ut-placeholder-label
app/assets/javascripts/components/o2/PlaceholderLabel.jsx
85: 'input-placeholder-label': true,
app/assets/stylesheets/p1/search/_SearchForm.scss
77: .input-placeholder-label {
321:.input-placeholder-label,
spec/javascripts/components/o2/PlaceholderLabel_spec.jsx
25: const placeholderContainer = wrapper.find('.input-placeholder-label');
Он быстро сузил поиск до o2/PlaceHolderLabel.jsx, компонента поиска в верхней части раздела комментариев. 🔍
Из кода я вижу, что мы обнаруживаем некоторые функции браузера, чтобы убедиться, что заполнитель виден в старых браузерах (например, Internet Explorer), но отображаем ввод по-разному, если он не поддерживается в текущем браузере. Поскольку браузер не может быть обнаружен на этапе рендеринга на стороне сервера, сервер всегда будет отображать дополнительный контент.
Это не только влияет на производительность, но и приводит к тому, что каждый раз отображаются дополнительные метки. Чтобы исправить это, я использую состояние реакции для рендеринга этой части и помещаю его в componentDidMount, который не будет выполняться до тех пор, пока клиент не отрендерит. 🥂
Снова запустив профилировщик, вы увидите, что
При обновлении один
export default class SummaryIconRow extends React.Component {
...
}
Измените его на:
export default class SummaryIconRow extends React.PureComponent {
...
}
Далее мы видим, что
Интересно, что эти компоненты невидимы, если клиенту не требуется ввод данных 👻.
Решение этой проблемы состоит в том, чтобы не выполнять рендеринг до тех пор, пока компонент не будет использован. Это увеличивает скорость первоначального рендеринга и повторного рендеринга. 🐎 Если мы копнем немного глубже и используем больше PureComponents, мы сможем ускорить рендеринг.
прокручивать вверх и вниз
Выполняя некоторую работу по оптимизации плавной анимации прокрутки, я заметил, что страница при прокрутке была очень прерывистой. 📜 Когда анимация не достигает 60 fps (кадров в секунду), дажеПользователи могут чувствовать заикание, пока не будет достигнута скорость 120 кадров в секунду..Прокрутка — это особый вид анимации, который напрямую связан с движением пальцев, поэтому он более чувствителен, чем другие анимации, когда производительность низкая.
Немного проанализировав, я обнаружил, что мы делаем много ненужного повторного рендеринга компонентов React в обработчиках событий прокрутки! Это очень плохо:
Замена трех компонентов в этих деревьях (
Кроме того, есть некоторые части, которые можно оптимизировать. 🚗 Немного расширив график пламени, мы видим, что мы по-прежнему тратим много времени на повторный рендеринг
Мы видим, что всего ссылок четыре, но только две требуют обновления внешнего вида при переходе между разделами. На графике пламени мы видим, что каждый раз перерисовываются четыре ссылки. Это происходит из-за того, что компонент
const anchors = React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
selected: activeAnchorIndex === index,
onPress(event) { onAnchorPress(index, event); },
});
});
Мы можем исправить это, убедившись, что
const anchors = React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
selected: activeAnchorIndex === index,
index,
onPress: this.handlePress,
});
});
В
class NavigationAnchor extends React.Component {
constructor(props) {
super(props);
this.handlePress = this.handlePress.bind(this);
}
handlePress(event) {
this.props.onPress(this.props.index, event);
}
render() {
...
}
}
Запустив профилировщик после оптимизации, можно увидеть, что перерисовываются только две ссылки, а нагрузка стала вдвое меньше предыдущей 🌗! Кроме того, если мы используем больше ссылок, объем работы, которую необходимо выполнить, не увеличивается.
FlexportизDounan ShiисследовалReflective Bind, который использует Плагин Babel для выполнения этого типа оптимизации. Этот проект все еще находится в зачаточном состоянии и еще не готов к официальному релизу, но я с нетерпением жду его будущего.
Глядя на главную панель инструментов производительности, я заметил, что у нас есть очень подозрительный блок _handleScroll, который занимает 19 мс на событие прокрутки. Если мы хотим достичь 60 кадров в секунду, у нас может быть только 16 мс времени рендеринга, что, очевидно, слишком много. 🌯
Виновник, похоже, находится внутри onLeaveWithTracking. Поискав код, я проследил его до
Копаясь в
this.state = { inViewport: false };
но,Мы никогда не используем inViewport в пути рендеринга, и нам никогда не нужно запускать повторный рендеринг при изменении inViewport, что означает, что мы платим ненужные накладные расходы на производительность.. 💸 Преобразование всех похожих вариантов использования состояния React в простые переменные экземпляра помогает ускорить анимацию прокрутки.
this.inViewport = false;
Я также заметил, что повторный рендеринг
В конце концов было подтверждено, что повторный рендеринг был вызван компонентом высшего порядка withExperiments, который помогал нам в наших экспериментах. Этот HOC всегда передает вновь созданный объект в качестве реквизита компоненту, который он обертывает, теряя любые оптимизации на своем пути.
render() {
...
const finalExperiments = {
...experiments,
...this.state.experiments,
};
return (
<WrappedComponent
{...otherProps}
experiments={finalExperiments}
/>
);
}
я представилreselectИсправлена проблема, из-за которой он кэшировал последний результат, сохраняя равенство ссылок между последовательными рендерингами.
const getExperiments = createSelector(
({ experimentsFromProps }) => experimentsFromProps,
({ experimentsFromState }) => experimentsFromState,
(experimentsFromProps, experimentsFromState) => ({
...experimentsFromProps,
...experimentsFromState,
}),
);
...
render() {
...
const finalExperiments = getExperiments({
experimentsFromProps: experiments,
experimentsFromState: this.state.experiments,
});
return (
<WrappedComponent
{...otherProps}
experiments={finalExperiments}
/>
);
}
Вторая часть вопроса аналогична. Мы использовали функцию getFilteredAmenities, которая принимает массив в качестве первого аргумента и возвращает отфильтрованную версию этого массива, например:
function getFilteredAmenities(amenities) {
return amenities.filter(shouldDisplayAmenity);
}
Хотя это выглядит нормально, каждый раз, когда он запускается, даже если результат один и тот же, создается новый экземпляр массива, и любые чистые компоненты, которые принимают массив в качестве параметра, не могут быть оптимизированы. Я решил эту проблему, введя повторный выбор для кэширования этого фильтра. На данный момент графика пламени больше нет, потому что весь повторный рендеринг полностью исчез! 👻
В дополнение к этому могут быть дополнительные возможности оптимизации (например,CSS containment), но производительность прокрутки значительно улучшилась!
нажмите действие
Продолжая больше взаимодействовать со страницей, я заметно ощутил задержку нажатия кнопки «Полезно» в комментариях ✈️.
Я предполагаю, что нажатие этой кнопки приведет к повторному отображению всех комментариев на странице. Взгляните на график пламени, как я и ожидал:
После использования React.PureComponent в нескольких местах обновления страниц стали более эффективными.
действие ввода
Возвращаясь к старой проблеме несоответствия сервер/клиент, я заметил вялую реакцию при вводе в поле ввода.
После анализа было обнаружено, что каждое нажатие клавиши приводит к повторному отображению всего заголовка области комментариев и каждого комментария! 😱 Это издеваешься?
Чтобы исправить это, я извлек часть заголовка раздела комментариев как компонент, чтобы использовать его как React.PureComponent, а затем разбросал эти React.PureComponent по дереву. Это заставляет каждое нажатие клавиши повторно отображать только компонент, который необходимо повторно отобразить: поле ввода.
Чему мы научились?
- Мы хотим, чтобы страницы запускались быстро и оставались плавными.
- Это означает, что нам нужно сосредоточиться не только на времени до интерактива, когда пользователь инициирует запрос к странице, но и на анализе интерактивных действий на странице, таких как прокрутка, нажатие и ввод текста.
- React.PureComponent и reselect — два очень полезных инструмента в процессе оптимизации приложений React.
- Следует избегать состояния реакции, когда переменные экземпляра — это именно то, что вам нужно.
- Хотя React является мощным, также легко написать код, который влияет на производительность.
- Выработайте привычку анализировать, оптимизировать и снова анализировать.