Хватит использовать производительность для расчета экранного времени! !

оптимизация производительности монитор
Хватит использовать производительность для расчета экранного времени! !

1. Предпосылки

некоторое время назад备战双十一前期, для онлайн-проектов性能问题привлек наше внимание

Внутри компании есть统一的性能监控平台Да, наши проекты также подключены к платформе мониторинга, но нам не совсем понятен метод расчета этого времени, поэтому мы нашли время для проведения углубленного исследования.

Результатом исследования является то, что методы расчета других значений времени (таких как время сетевого запроса, время первого пакета...) относительно ясны.показать путь,Кроме首屏时间, в отрасли нет единого стандарта

После исследования首屏时间的计算方式Это все еще очень хардкорно, и недавно у меня было время записать и поделиться им~

В этой статье рассказывается о схеме подсчета времени первого экрана в интерфейсе, она реализована по частичному алгоритму, а акцент сделан на идеях, понять - значит заработать!

image.png

Во-вторых, какое первое экранное время

首屏时间: также называемыйПолностью интерактивное время пользователя, то есть первый экран всей страницы полностью отображается, и пользователь может полноценно взаимодействовать. Как правило, время первого экрана меньше, чем время полной загрузки страницы. Значение этого показателя может измерять скорость доступа к странице.

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