1. Предпосылки
некоторое время назад备战双十一前期
, для онлайн-проектов性能问题
привлек наше внимание
Внутри компании есть统一的性能监控平台
Да, наши проекты также подключены к платформе мониторинга, но нам не совсем понятен метод расчета этого времени, поэтому мы нашли время для проведения углубленного исследования.
Результатом исследования является то, что методы расчета других значений времени (таких как время сетевого запроса, время первого пакета...) относительно ясны.показать путь,Кроме首屏时间
, в отрасли нет единого стандарта
После исследования首屏时间的计算方式
Это все еще очень хардкорно, и недавно у меня было время записать и поделиться им~
В этой статье рассказывается о схеме подсчета времени первого экрана в интерфейсе, она реализована по частичному алгоритму, а акцент сделан на идеях, понять - значит заработать!
Во-вторых, какое первое экранное время
首屏时间
: также называемыйПолностью интерактивное время пользователя, то есть первый экран всей страницы полностью отображается, и пользователь может полноценно взаимодействовать. Как правило, время первого экрана меньше, чем время полной загрузки страницы. Значение этого показателя может измерять скорость доступа к странице.
1. Время первого экрана VS время белого экрана
Это два совершенно разных понятия,Время белого экрана меньше, чем время первого экрана
白屏时间
: Время первого рендеринга, которое означает время, необходимое для появления первого текста или изображения на странице.
2. Почему производительность не может напрямую получить первое экранное время?
С преобладанием интерфейсных фреймворков, таких как Vue и React,Performance
Он не смог точно контролировать время экрана на первую страницу
потому чтоDOMContentLoaded
Значение может представлять толькопустая страница(В текущем теге body страницы нет содержимого) Время, необходимое для загрузки
Браузер должен загрузить JS, а затем сделать содержимое страницы через JS, на этот разПервый экран не отображается до тех пор, пока первый экран не будет завершен.
3. Общие методы расчета
- Пользовательское управление - наиболее точный способ (только пользователь лучше знает, во сколько завершается первая загрузка экрана)
- Недостатки: Навязчивый бизнес, высокая стоимость
- Примерно рассчитайте время над сгибом:
loadEventEnd - fetchStart/startTime
илиdomInteractive - fetchStart/startTime
- Рассчитав время загрузки всех изображений в верхней части сгиба, а затем взяв максимальное значение
- использоватьMutationObserverИнтерфейс для мониторинга изменений узла объекта документа
4. Наша схема расчета
использоватьMutationObserverинтерфейс, прослушиваниеDOM
Узловые изменения объектов
Совет: Алгоритм более сложный, статья пытается выразить как можно больше способов понять, процесс анализа максимально прост, реальная ситуация сложнее, чем эта
Во-первых, предположим页面DOM
Окончательная структура выглядит следующим образом: страницаdom深度为3
<body>
<div>
<div>
<div>1</div>
<div>2</div>
</div>
<div>3</div>
<div style="display: none;">4</div>
</div>
<ul>
<li>1</li>
<li>2</li>
</ul>
</body>
1. Инициализируйте прослушиватель MutationObserver
Код инициализации выглядит следующим образом
- Если текущий браузер не поддерживает
MutationObserver
отказаться от отчетности -
this.startTimeвзятый
window.performance.getEntriesByType('navigation')[0].startTime
,Прямо сейчасНачать запись времени выступления -
this.observerData
Массив используется для записи времени каждого изменения DOM и оценки изменения (серьезности изменения).
function mountObserver () {
if (!window.MutationObserver) {
// 不支持 MutationObserver 的话
console.warn('MutationObserver 不支持,首屏时间无法被采集');
return;
}
// 每次 dom 结构改变时,都会调用里面定义的函数
const observer = new window.MutationObserver(() => {
const time = getTimestamp() - this.startTime; // 当前时间 - 性能开始计算时间
const body = document.querySelector('body');
let score = 0;
if (body) {
score = traverseEl(body, 1, false);
this.observerData.push({ score, time });
} else {
this.observerData.push({ score: 0, time });
}
});
// 设置观察目标,接受两个参数: target:观察目标,options:通过对象成员来设置观察选项
// 设为 childList: true, subtree: true 表示用来监听 DOM 节点插入、删除和修改时
observer.observe(document, { childList: true, subtree: true });
this.observer = observer;
if (document.readyState === 'complete') {
// MutationObserver监听的最大时间,10秒,超过 10 秒将强制结束
this.unmountObserver(10000);
} else {
win.addEventListener(
'load',
() => {
this.unmountObserver(10000);
},
false
);
}
}
Mutation
При первом прослушивании изменения DOMDOM结构
Как следует, вы можете видетьdiv标签
оказанный
<body>
<div>
<div>
<div>1</div>
<div>2</div>
</div>
<div>3</div>
<div style="display: none;">4</div>
</div>
</body>
траверсbody
элемент под, через методtraverseEl
Рассчитывайте каждый раз, когда вы слушаетеDOM
Оценка при смене, алгоритм следующий
2. Рассчитайте оценку при изменении DOM
функция вычисленияtraverseEl
следующим образом
- от
body
Элементы начинают вычисляться рекурсивно, первый вызовtraverseEl(body, 1, false)
- Исключите бесполезные узлы элементов, такие как
script
,style
,meta
,head
-
layer
Указывает текущийDOM层数
, оценка каждого слоя равна1 + (层数 * 0.5)
+该层children的所有得分
- Если высота элемента превышает видимую высоту экрана, вернуть 0 баллов напрямую, то есть при первом вызове элемента, если высота элемента превысила видимую высоту экрана, он сразу вернет 0
/**
* 深度遍历 DOM 树
* 算法分析
* 首次调用为 traverseEl(body, 1, false);
* @param element 节点
* @param layer 层节点编号,从上往下,依次表示层数
* @param identify 表示每个层次得分是否为 0
* @returns {number} 当前DOM变化得分
*/
function traverseEl (element, layer, identify) {
// 窗口可视高度
const height = win.innerHeight || 0;
let score = 0;
const tagName = element.tagName;
if (
tagName !== 'SCRIPT' &&
tagName !== 'STYLE' &&
tagName !== 'META' &&
tagName !== 'HEAD'
) {
const len = element.children ? element.children.length : 0;
if (len > 0) {
for (let children = element.children, i = len - 1; i >= 0; i--) {
score += traverseEl(children[i], layer + 1, score > 0);
}
}
// 如果元素高度超出屏幕可视高度直接返回 0 分
if (score <= 0 && !identify) {
if (
element.getBoundingClientRect &&
element.getBoundingClientRect().top >= height
) {
return 0;
}
}
score += 1 + 0.5 * layer;
}
return score;
}
Рассчитать оценку при первом изменении DOMscore = traverseEl(body, 1, false)
Таким образом, вы можете видеть, что оценка изменения8.5
Результаты сохраняются вthis.observerData
серединаthis.observerData.push({ score, time })
body =》 traverseEl(body, 1, false); score = 8.5;
div =》 traverseEl(div, 2, false); score = 8.5;
div =》 traverseEl(div, 3, false); score = 6;
div =》 traverseEl(div, 4, false); score = 3;
div =》 traverseEl(div, 4, false); score = 3;
div =》 traverseEl(div, 3, false); score = 2.5;
div =》 traverseEl(div, 3, false); score = 0;
Mutation
Во второй раз, когда прослушиватель DOM изменится, вы можете увидетьul标签
также предоставлено
<body>
<div>
<div>1</div>
<div>2</div>
<div style="display: none;">3</div>
</div>
<ul>
<li>1</li>
<li>2</li>
</ul>
</body>
такой же счетscore = traverseEl(body, 1, false)
, вы можете видеть, что показатель изменения равен10
сохранить счет в数组this.observerData
середина
body =》 traverseEl(body, 1, false); score = 10;
div =》 traverseEl(div, 2, false); score = 5;
div =》 traverseEl(div, 3, false); score = 2.5;
div =》 traverseEl(div, 3, false); score = 2.5;
div =》 traverseEl(div, 3, false); score = 0;
ul =》 traverseEl(div, 2, false); score = 5;
li =》 traverseEl(div, 3, false); score = 2.5;
li =》 traverseEl(div, 3, false); score = 2.5;
Есть один до сих порDOM
изменение массиваthis.observerData
На самом деле прослушиватель Mutation будет вызываться несколько раз, и будут элементы с одинаковыми оценками.
3. Убрать мониторинг удаления DOM
Сначала удалите элемент, который меньше предыдущего, т.е.Удалить прослушиватель для удаления DOM, потому что если в процессе рендеринга страницы будет удалено большое количество DOM-узлов, они будут проигнорированы из-за малого балла
Например[3,4,2,3,1,5,3]
, результат[3,4,5]
/**
* @param observerData
* @returns {*}
*/
function removeSmallScore (observerData) {
for (let i = 1; i < observerData.length; i++) {
if (observerData[i].score < observerData[i - 1].score) {
observerData.splice(i, 1);
return removeSmallScore(observerData);
}
}
return observerData;
}
4. ВозьмиDOM变化最大
Момент времени – это время первого экрана.
пройти по очередиobserverData
,если下一个得分score
а также前一个得分score
Разница больше, чем data.rate
Это означает, что новый элемент dom отрисовывается на странице позже, а в следующий раз берется
Таким образом, можно исключить рендеринг анимированных элементов, карусельных изображений и т. д., а время рендеринга первого экрана можно рассчитать более точно.
Таким образом, вы не можете напрямую взять время последнего элемента, т.е.observerData[observerData.length-1].score
function getfirstScreenTime = {
this.observerData = removeSmallScore(this.observerData);
let data = null;
const { observerData } = this;
for (let i = 1; i < observerData.length; i++) {
if (observerData[i].time >= observerData[i - 1].time) {
const scoreDiffer =
observerData[i].score - observerData[i - 1].score;
if (!data || data.rate <= scoreDiffer) {
data = { time: observerData[i].time, rate: scoreDiffer };
}
}
}
if (data && data.time > 0 && data.time < 3600000) {
// 首屏时间
this.firstScreenTime = data.time;
}
}
5. Действия в нештатных ситуациях
Если нет отчета, когда страница закрыта, сообщите об этом немедленно
-
window
мониторbeforeunloadСобытие (когда окно браузера закрывается или обновляется, срабатывает событие beforeunload) -
this.calcFirstScreenTime
Для расчета первого экрана состояния времени, разделенногоinit
,pending
,а такжеfinished
три состояния -
this.calcFirstScreenTime = pending
СрабатываетunmountObserver
Немедленная эскалация и удаление инцидентов
window.addEventListener('beforeunload', this.unmountObserverListener);
const unmountObserverListener = () => {
if (this.calcFirstScreenTime === 'pending') {
this.unmountObserver(0, true);
}
if(!isIE()){
window.removeEventListener('beforeunload', this.unmountObserverListener);
}
};
6. Уничтожить MutationObserver
Давайте посмотрим卸载MutationObserver
Что ты делал, когдаunmountObserver
Этот метод определит, следует ли удалитьif (immediately || this.compare(delayTime))
Поскольку выгрузка немедленно возвращает значение true и, наконец, вычисляет заданное время; если оно возвращает значение false,Опрос unmountObserver через 500 мс
this.observer.disconnect()
Хватит следить за изменениями,MutationObserver.disconnect()
/**
* @param delayTime 延迟的时间
* @param immediately 指是否立即卸载
* @returns {number}
*/
function unmountObserver (delayTime, immediately) {
if (this.observer) {
if (immediately || this.compare(delayTime)) {
// MutationObserver停止观察变动
this.observer.disconnect();
this.observer = null;
this.getfirstScreenTime()
this.calcFirstScreenTime = 'finished';
} else {
setTimeout(() => {
this.unmountObserver(delayTime);
}, 500);
}
}
}
// * 如果超过延迟时间 delayTime(默认 10 秒),则返回 true
// * _time - time > 2 * OBSERVE_TIME; 表示当前时间与最后计算得分的时间相比超过了 1000 毫秒,则说明页面 DOM 不再变化,返回 true
function compare (delayTime) {
// 当前所开销的时间
const _time = Date.now() - this.startTime;
// 取最后一个元素时间 time
const { observerData } = this;
const time =
(
observerData &&
observerData.length &&
observerData[observerData.length - 1].time) ||
0;
return _time > delayTime || _time - time > 2 * 500;
}
напиши в конце
Выше приведена схема расчета первого экранного времени в этой статье, добро пожаловать на обсуждение~
Эта статья была впервые опубликована вGitHub