Оптимизация производительности браузера — производительность рендеринга

внешний интерфейс программист JavaScript браузер Язык программирования Chrome HTML CSS
Оптимизация производительности браузера — производительность рендеринга

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

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

Плавная анимация должна поддерживаться на уровне 60 кадров в секунду, которые преобразуются в миллисекунды, и браузер должен выполнить задачу рендеринга примерно за 10 миллисекунд (есть 1000 миллисекунд в секунду, 1000/60 — это около 16 миллисекунд на кадр, но браузер по-прежнему нуждается в другой работе.Это занимает время, поэтому оценивается в 10 мс), если вы можете понять процесс рендеринга браузера, найти узкое место в производительности и оптимизировать его, вы можете сделать свой проект интерактивным, а эффект анимации шелковистым. гладкий.

Автор этой статьи:SylvanasSun(sylvanas.sun@gmail.com).Для перепечатки просим разместить этот абзац в начале статьи (сохранить гиперссылку).
Эта статья была впервые опубликована сSylvanasSun Blog, оригинальная ссылка:sylva, то есть sun.GitHub.IO/2017/10/08/…

пиксельный конвейер


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

像素管道
пиксельный конвейер

  • JavaScript:Эта область на самом деле относится к методу достижения эффекта анимации., обычно используетсяJavaScriptанимировать, напр.JQueryизanimateфункция, сортировка набора данных или динамическое добавление некоторыхDOMузел и т. д. Конечно, для достижения анимационных эффектов можно использовать и другие методы, такие какCSSизAnimation,Transitionа такжеTransform.

  • Стиль:Эта область является этапом расчета стиля, и браузер будет использовать селектор (т. е.CSSселектор, например.td), чтобы выяснить, какие узлы применяются к какимCSSrules, затем вычислите окончательный стиль для каждого узла и примените его к узлу.

  • Макет:Эта область является этапом расчета макета, во время которого браузер рассчитает размер занимаемого пространства и положение на экране в соответствии со стилевыми правилами ноды..

  • Краска:Эта область является фазой рисования, браузер сначала создаст список вызовов отрисовки, а затем заполнит пиксели.. Фаза рисования включает в себя текст, цвета, изображения, границы и тени, практически каждую видимую часть. Рисунок обычно выполняется на нескольких слоях (используетсяPhotoshopСлой слова должен быть знаком детской обуви программного обеспечения для редактирования изображений, и значение слоя здесь на самом деле аналогично).

  • Композитный:Эта область является фазой компоновки, когда браузер выводит на экран несколько слоев в правильном порядке.

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

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

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

Если вы хотите знать каждыйCSSКак свойства повлияют на какой этап, перейти кCSS Triggers, сайт подробно описывает каждыйCSSНа какой этап влияет свойство.

Анимация с помощью функции RequestAnimationFrame


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

избегать использованияsetTimeout()илиsetInterval()функция для достижения эффекта анимации, основная проблема с этим подходомОбратный вызов будет выполняться в какой-то момент кадра, который может быть прямо в конце (кадры будут потеряны, что приведет к заиканию).

Некоторые сторонние библиотеки все еще используютсяsetTimeout()&setInterval()функция для достижения анимационных эффектов, что приведет к ненужному снижению производительности, например, в старой версииJQuery, если вы используетеJQuery3, то не беспокойтесь об этом,JQuery3Модуль анимации был полностью переписан с использованиемrequestAnimationFrame()функция для достижения эффекта анимации. Но если вы используете предыдущую версиюJQuery, то вам нужноjquery-requestAnimationFrameбудущееsetTimeout()заменитьrequestAnimationFrame()функция.

Прочитав это, вы должны бытьrequestAnimationFrame()Полюбопытствовать. Чтобы получить плавную анимацию, мы хотим, чтобы визуальное изменение происходило в начале каждого кадра, обеспечивая при этомJavaScriptСпособ запуска в начале кадра заключается в использованииrequestAnimationFrame()функция, которая по существу аналогичнаsetTimeout()Нет никакой разницы, все они рекурсивно вызывают одну и ту же функцию обратного вызова, чтобы постоянно обновлять экран для достижения эффекта анимации.requestAnimationFrame()используется следующим образом:

function updateScreen(time) {
    // 这是你的动画效果函数
}

// 将你的动画效果函数放入requestAnimationFrame()作为回调函数
requestAnimationFrame(updateScreen);

Не все браузеры поддерживаютrequestAnimationFrame()функции, такие какIE9(опять злоIE), но в основном современные браузеры поддерживают эту функцию. Если вам нужна совместимость со старыми браузерами, вы можете использовать следующую функцию.

// 本段代码截取自Paul Irish : https://gist.github.com/paulirish/1579671
(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
    }

    // 如果浏览器不支持,则使用setTimeout()
    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };

    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());

Web Workers


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

использоватьChromeинструменты разработчикаTimelineфункция, которая поможет нам увидеть каждыйJavaScriptВремя выполнения скриптов (включая индексы) помогает нам находить и устранять узкие места в производительности.

数据采自掘金
Данные из самородков

Поиск влиятельной производительностиJavaScriptПосле скрипта можем пройтиWeb Workersоптимизировать.Web WorkersдаHTML5предлагаемый стандарт, которыйможет позволитьJavaScriptСкрипт выполняется в фоновом потоке (аналогично созданию дочернего потока), и фоновый поток не влияет на страницу в основном потоке.. но,использоватьWeb WorkersСозданный поток не может работатьDOMдерево(это тожеWeb Workersнет подрывной деятельностиJavaScriptявляется причиной одиночного потока,JavaScriptПричина, по которой он всегда был однопоточным, в основном состоит в том, чтобы избежать нескольких операций скрипта.DOMПроблема синхронизации дерева, это значительно усложнит), поэтому подходит только для некоторых чистых вычислений (сортировка данных, обход и т. д.).

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

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);

function processTaskList(taskStartTime) {
  var taskFinishTime;

  do {
    // 从列表中弹出任务
    var nextTask = taskList.pop();

    // 执行任务
    processTask(nextTask);

    // 如果有足够的时间进行下一个任务则继续执行
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);

  if (taskList.length > 0)
    requestAnimationFrame(processTaskList);

}

СоздаватьWeb WorkersОбъекты простые, просто позвонитеWorker()конструктор, а затем передать указанный скриптURI. Современные основные браузеры поддерживаютWeb Workers,КромеInternet Explorer(Это снова злой IE), поэтому нам также нужно проверить совместимость браузера в следующем примере кода.

var myWorker;

if (typeof(Worker) !== "undefined") {
    // 支持Web Workers
    myWorker = new Worker("worker.js");
} else {
    // 不支持Web Workers
}

Web Workersчерез основной потокpostMessage()функция отправки информации, используйтеonmessage()Обработчики событий для ответа на сообщения (данные не распределяются между основным потоком и подпотоками, они просто взаимодействуют, копируя данные).

main.js: 
// 在主线程js中发送数据到myWorker绑定的js脚本线程
myWorker.postMessage("Hello,World");
console.log('Message posted to worker');

worker.js:
// onmessage处理函数允许我们在任何时刻,
// 一旦接收到消息就可以执行一些代码,代码中消息本身作为事件的data属性进行使用。
onmessage = function(data) {
    console.log("Message received from main script.");
    console.log("Posting message back to main script.");
    postMessage("Hello~");
}

main.js:
// 主线程使用onmessage接收消息
myWorker.onmessage = function(data) {
    console.log("Received message: " + data);
}

Если вам нужно немедленно завершить работающий воркер из основного потока, вызовите метод воркераterminate()функция:

myWorker.terminate();

myWorker будет немедленно уничтожен, и у него не будет возможности продолжить выполнение остальной работы. И его также можно вызвать в рабочем потокеclose()функция закрытия:

close();

для большегоWeb WorkersДля использования см.Using Web Workers - Web APIs | MDN.

Уменьшить сложность расчета стилей


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

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

Скорость расчета стиля можно повысить, уменьшив сложность селекторов.

Ниже представлен комплексCSSСелектор:

.box:nth-last-child(-n+1) .title {
  /* styles */
}

Если вы хотите, чтобы браузер узла применил стиль, вам нужно найти.titleУзел класса, тогда его родитель точно отрицательный n дочерних элементов + 1 полоса.boxузел класса. Браузеру может потребоваться значительное время для вычисления этого результата, но мы можем изменить ожидаемое поведение селектора на класс:

.final-box-title {
  /* styles */
}

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

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

BEMНа самом деле этоBlock,Element,Modifier, который представляет собой компонентный подход к разработке, его идея состоит в том, чтобы разделить пользовательский интерфейс на независимые части. Таким образом, даже используя сложныеUIЕго также можно легко и быстро разработать, а модульный подход может повысить возможность повторного использования кода.

Blockявляется функционально независимым компонентом страницы (который можно использовать повторно),Blockназывается как письмоClassто же имя. как показано ниже.buttonпредставляет<button>изBlock.

.button {
    background-color: red;
}

<button class="button">I'm a button</button>

Elementтот, который нельзя использовать в одиночкуBlockсоставная часть. Это можно считатьElementдаBlockдочерний узел .

<!-- `search-form`是一个block -->
<form class="search-form">
    <!-- 'search-form__input'是'search-form' block中的一个element -->
    <input class="search-form__input">

    <!-- 'search-form__button'是'search-form' block中的一个element  -->
    <button class="search-form__button">Search</button>
</form>

Modifierиспользуется для определенияBlockилиElementвнешний вид, состояние или поведение объекта. Предположим, у нас есть новое требование дляbuttonцвет фона с использованием зеленого, тогда мы можем использоватьModifierправильно.buttonСделайте расширение:

.button {
    background-color: red;
}

.button--secondary {
    background-color: green;
}

первый контактBEMдетской обуви это название может показаться странным, ноBEMВажна идея модульности и ремонтопригодности, Что касается именования, вы можете изменить его так, как вы можете принять. Из-за нехватки места в этой статье мы не будем продолжать обсуждениеBEMТеперь заинтересованные детские туфли могут пойти посмотретьОфициальная документация БЭМ.

Избегайте принудительной синхронизации макета и дрожания макета


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

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

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

Например, следующий код выводит высоту элемента в начале каждого кадра:

requestAnimationFrame(logBoxHeight);

function logBoxHeight() {
  console.log(box.offsetHeight);
}

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

function logBoxHeight() {
  box.classList.add('super-big');

  console.log(box.offsetHeight);
}

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

function logBoxHeight() {
  console.log(box.offsetHeight);

  box.classList.add('super-big');
}

Дрожание макета возникает, когда принудительная синхронная компоновка выполняется одна за другой.. Следующий код перебирает набор абзацев и устанавливает ширину каждого абзаца в соответствии с шириной элемента с именем «box».

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}

Проблема с этим кодом в том, что каждая итерация считываетbox.offsetWidth, затем немедленно используйте это значение для обновления ширины абзаца. На следующей итерации цикла браузер должен учитывать тот факт, что стиль обновляется (box.offsetWidthбыл запрошен в предыдущей итерации), поэтому он должен применить изменения стиля, а затем выполнить макет. Это приводит к принудительному синхронному макету на каждой итерации, правильный способ сделать это — сначала прочитать значение, а затем записать значение.

// Read.
var width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    // Now write.
    paragraphs[i].style.width = width + 'px';
  }
}

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

Анимация с использованием свойств, которые не запускают компоновку и рисование


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

Следует отметить, что использованиеtransformа такжеopacity, элемент, на котором изменяются эти свойства, должен находиться в своем собственном слое,Поэтому нам нужно создать новый слой для анимированного элемента (преимущество этого в том, что перерисовка на этом слое может быть обработана, не затрагивая элементы на других слоях. Если вы использовалиPhotoshop, вы должны понимать удобство работы с несколькими слоями).

Лучший способ создать новый слой — использоватьwill-changeАтрибуты,Этот атрибут сообщает браузеру, какие изменения будут внесены в элемент, чтобы браузер мог заранее подготовиться к соответствующей оптимизации до фактического изменения атрибута элемента.

.moving-element {
  will-change: transform;
}

// 对于不支持 will-change 但受益于层创建的浏览器,需要使用(滥用)3D 变形来强制创建一个新层
.moving-element {
  transform: translateZ(0);
}

но не думайwill-changeМожет улучшить производительность, просто злоупотребляйте им, используйтеwill-changeВыполнение предварительной оптимизации и создание слоев требует дополнительной памяти и затрат на управление, а неправильное их использование только окупится.

использованная литература