Высокопроизводительный внешний рендеринг 100 000 фрагментов данных (временной разрез)

JavaScript оптимизация производительности
Высокопроизводительный внешний рендеринг 100 000 фрагментов данных (временной разрез)

Чем больше вы знаете, тем больше вы не знаете
点赞Посмотри еще раз, аромат остался в руке, и слава

предисловие

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

В случае одновременной вставки большого количества данных обычно существует два подхода:

  1. разрезание времени
  2. виртуальный список

В начале этой статьи мы сосредоточимся на том, как использовать时间分片способ рендеринга больших объемов данных,虚拟列表Связанный контент см.«Передний расширенный» высокопроизводительный рендеринг 100 000 элементов данных (виртуальный список)

Самый грубый подход (однократный рендеринг)

Давайте сначала рассмотрим самый грубый подход, вставляя сразу большой объем данных на страницу:

<ul id="container"></ul>
// 记录任务开始时间
let now = Date.now();
// 插入十万条数据
const total = 100000;
// 获取容器
let ul = document.getElementById('container');
// 将数据插入容器中
for (let i = 0; i < total; i++) {
    let li = document.createElement('li');
    li.innerText = ~~(Math.random() * total)
    ul.appendChild(li);
}

console.log('JS运行时间:',Date.now() - now);
setTimeout(()=>{
  console.log('总运行时间:',Date.now() - now);
},0)
// print: JS运行时间: 187
// print: 总运行时间: 2844

Мы зацикливаем более 100 000 записей, а время работы JS составляет187ms, что по-прежнему довольно быстро, но общее время после окончательного рендеринга составляет2844ms.

Кратко объясните, почему дваждыconsole.logРезультаты времени сильно разнятся, и как просто их посчитатьJS运行时间а также总渲染时间:

  • в JSEvent Loop, когда все события в стеке выполнения, управляемом движком JS, и все события микрозадач выполняются, поток рендеринга будет запущен для рендеринга страницы.
  • Первыйconsole.logВремя триггера — до отображения страницы, а интервал времени, полученный в это время, — это время, необходимое для запуска JS.
  • второйconsole.logОн помещается в setTimeout, и его время срабатывания — когда рендеринг завершен, а в следующий разEvent Loopказнен в

Подробнее о цикле событий см. в этой статье -->

согласно дваждыconsole.log, можно сделать вывод, что:

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

Используйте таймер

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

Здесь мы используемsetTimeoutдля пакетного рендеринга

<ul id="container"></ul>
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
    if(curTotal <= 0){
        return false;
    }
    //每页多少条
    let pageCount = Math.min(curTotal , once);
    setTimeout(()=>{
        for(let i = 0; i < pageCount; i++){
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            ul.appendChild(li)
        }
        loop(curTotal - pageCount,curIndex + pageCount)
    },0)
}
loop(total,index);

Используйте гифку, чтобы увидеть эффект

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

Почему появляется заставка?

Во-первых, проясните некоторые понятия.FPSУказывает количество обновлений экрана в секунду. Непрерывные изображения, которые мы обычно видим, состоят из неподвижных изображений, и каждое изображение называется,FPSэто описаниеФизическая величина, меняющая скорость.

Большинство компьютерных мониторов имеют частоту обновления 60 Гц, что примерно эквивалентно 60 перерисовкам в секунду.FPSЭто 60 кадров/с, настройка этого значения зависит от разрешения экрана, размера экрана и видеокарты.

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

Почему ты не чувствуешь перемен?

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

И ощущение, которое дает экран, правильное, только представьте, если частота обновления станет 1 раз/сек, то изображение на экране будет серьезно мерцать. Это может легко привести к таким симптомам, как усталость глаз, болезненность и головокружение.

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

Интуитивное ощущение, опыт работы с разной частотой кадров:

  • Анимации с частотой кадров от 50 до 60 FPS будут достаточно плавными и комфортными;
  • Для анимаций с частотой кадров от 30 до 50 кадров в секунду уровень комфорта варьируется от человека к человеку из-за их разных уровней чувствительности;
  • Анимации с частотой кадров ниже 30 кадров в секунду заставляют людей чувствовать явные заикания и дискомфорт;
  • Анимации с большими колебаниями частоты кадров также могут заставить людей чувствовать себя застрявшими.

Коротко о setTimeout и феномене заставки

  • setTimeoutВремя выполнения не является детерминированным. В JS,setTimeoutЗадача помещается в очередь событий, и только после выполнения основного потока он проверяет, нужно ли выполнять задачу в очереди событий, поэтомуsetTimeoutФактическое время выполнения может быть позже установленного времени.
  • На частоту обновления влияет разрешение экрана и размер экрана, поэтому она может варьироваться от устройства к устройству, в то время какsetTimeoutМожно установить только фиксированный интервал времени, который не обязательно совпадает со временем обновления экрана.

Две вышеупомянутые ситуации приведут к тому, что темп выполнения setTimeout будет несовместим с темпом обновления экрана.

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

Используйте запросanimationFrame.

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

Если частота обновления экрана 60 Гц, то функция обратного вызова будет выполняться каждые 16,7 мс, если частота обновления 75 Гц, то временной интервал становится равным 1000/75=13,3 мс, другими словами,requestAnimationFrameТемп соответствует темпу обновления системы. Это может гарантировать, что функция обратного вызова выполняется только один раз в каждом интервале обновления экрана, чтобы не вызвать потерю кадров.

Мы используемrequestAnimationFrameЧтобы выполнить пакетный рендеринг:

<ul id="container"></ul>
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
    if(curTotal <= 0){
        return false;
    }
    //每页多少条
    let pageCount = Math.min(curTotal , once);
    window.requestAnimationFrame(function(){
        for(let i = 0; i < pageCount; i++){
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            ul.appendChild(li)
        }
        loop(curTotal - pageCount,curIndex + pageCount)
    })
}
loop(total,index);

увидеть эффект

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

Это конец, можно снова оптимизировать?

Конечно~~

Использование фрагментов документа

Сначала объясните, что такое документфрагмент, ссылки на литературуMDN

DocumentFragment, Интерфейс фрагментов документа представляет собой минимальный объект документа, не имеющий родительского файла. Используется как облегченная версияDocumentИспользуется для хранения форматированных или неформатированных фрагментов XML. Самая большая разница в том, чтоDocumentFragmentНе являясь частью реального дерева DOM, его изменения не вызовут (повторного рендеринга) дерева DOM и не вызовут проблем с производительностью и т. д.
можно использоватьdocument.createDocumentFragmentметод или конструктор для создания пустогоDocumentFragment

Из описания MDN мы знаем, чтоDocumentFragmentsЭто узел DOM, но он не является частью дерева DOM и может считаться находящимся в памяти, поэтому вставка дочерних элементов во фрагмент документа не приведет к переформатированию страницы.

когдаappendэлемент кdocumentсередина, поappendВычисление таблицы стилей входящего элемента происходит синхронно, и вычисленное значение стиля можно получить, вызвав в это время getComputedStyle. а такжеappendэлемент кdocumentFragmentВ таблице стилей таблица стилей элемента не рассчитывается, поэтомуdocumentFragmentЛучшая производительность. Конечно, оптимизация браузера сейчас сделана очень хорошо, когдаappendэлемент кdocumentПосле середины современный браузер также может откладывать вычисление таблицы стилей после выполнения скрипта, когда нет возможности получить доступ к getComputeStyle.

Последний измененный код выглядит следующим образом:

<ul id="container"></ul>
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
    if(curTotal <= 0){
        return false;
    }
    //每页多少条
    let pageCount = Math.min(curTotal , once);
    window.requestAnimationFrame(function(){
        let fragment = document.createDocumentFragment();
        for(let i = 0; i < pageCount; i++){
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            fragment.appendChild(li)
        }
        ul.appendChild(fragment)
        loop(curTotal - pageCount,curIndex + pageCount)
    })
}
loop(total,index);

наконец

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

Рекомендуемая серия статей

Ссылаться на