В последнее время я работаю над более общей платформой мониторинга производительности внешнего интерфейса.В отличие от ненормального мониторинга внешнего интерфейса, мониторинг производительности внешнего интерфейса в основном должен сообщать и отображать данные о производительности внешнего интерфейса, включая время рендеринга домашней страницы, время белого экрана каждой страницы и данные о производительности каждой страницы, время загрузки всех ресурсов и время ответа на все запросы на каждой странице и т. д.
В этой статье рассказывается, как разработать общий jssdk, который может автоматически сообщать данные о производительности внешнего интерфейса с меньшим вмешательством. Основные из них — Performance API и метод sendBeacon. Основным ориентиром является практика использования аналитики Google и платформы мониторинга производительности внешнего интерфейса Alibaba Cloud.
В качестве бэкэнд-фреймворка в моем проекте я использую Nestjs.Nestjs — это бэкенд-фреймворк для узлов, основанный на Express, который отлично поддерживает машинописный текст и похож на java Spring. В этой статье основное внимание уделяется тому, как сообщать данные о производительности.Логика внутренней обработки относительно проста и не будет подробно описана, поэтому нет необходимости знать, как использоватьnesjs. Основное содержание этой статьи включает в себя:
- Получите данные о производительности внешнего интерфейса на основе Performance API
- Когда следует сообщать данные о производительности
- Как сообщить данные об эффективности
Оригинальный текст в моем блоге, приветствую звезду
1. Получите данные о производительности внешнего интерфейса в соответствии с Performance API.
Данные о производительности внешнего интерфейса, представленные в этой статье, состоят из двух частей: одна — это данные о производительности, полученные с помощью Performance API, а другая — это пользовательские данные, которые должны сообщаться на каждой странице.
Во-первых, посмотрите на данные, полученные на Performance API, которые также включают в себя две части, данные, связанные с производительностью, связанные с текущими страницами, а также связанные данные для текущей страницы ресурса нагрузки и асинхронного запроса.
(1), данные о производительности, предоставленные Performance API
window.performance.timing возвращает объект, содержащий различные данные, связанные с отрисовкой страницы. В этой статье мы не будем подробно знакомить с объектом, а только дадим метод расчета соответствующих данных о производительности в зависимости от объекта:
let times = {};
let t = window.performance.timing;
//重定向时间
times.redirectTime = t.redirectEnd - t.redirectStart;
//dns查询耗时
times.dnsTime = t.domainLookupEnd - t.domainLookupStart;
//TTFB 读取页面第一个字节的时间
times.ttfbTime = t.responseStart - t.navigationStart;
//DNS 缓存时间
times.appcacheTime = t.domainLookupStart - t.fetchStart;
//卸载页面的时间
times.unloadTime = t.unloadEventEnd - t.unloadEventStart;
//tcp连接耗时
times.tcpTime = t.connectEnd - t.connectStart;
//request请求耗时
times.reqTime = t.responseEnd - t.responseStart;
//解析dom树耗时
times.analysisTime = t.domComplete - t.domInteractive;
//白屏时间
times.blankTime = t.domLoading - t.fetchStart;
//domReadyTime
times.domReadyTime = t.domContentLoadedEventEnd - t.fetchStart;
Приведенный выше объект времени содержит атрибуты, связанные с производительностью, и результат можно получить, вычислив соответствующие атрибуты в performance.timing. Здесь мы думаем, что domReadyTime — это время загрузки первого экрана, кроме того, время первого экрана также можно сообщить в пользовательском методе:
Например, в некоторых сценариях можно считать, что точкой с наибольшим приращением DOM является время, когда завершен рендеринг первого экрана, а также есть некоторые сценарии, где видимый DOM может быть определен как время, когда первый экран рендеринг завершается в точке с наибольшим приращением.
(2) Загрузка ресурсов и данные запроса, предоставляемые Performance API.
Вы можете получить данные, связанные с загрузкой и запросом ресурсов, через window.performance.getEntries(). На каждой странице необходимо загрузить множество ресурсов, таких как js, css и т. д., а также на странице будут некоторые асинхронные запросы. Данные, связанные с этими загрузками ресурсов и асинхронными запросами, можно получить с помощью window.performance.getEntries(). Мы можем получить данные для загрузки и асинхронных запросов следующими способами:
let entryTimesList = [];
let entryList = window.performance.getEntries();
entryList.forEach((item,index)=>{
let templeObj = {};
let usefulType = ['navigation','script','css','fetch','xmlhttprequest','link','img'];
if(usefulType.indexOf(item.initiatorType)>-1){
templeObj.name = item.name;
templeObj.nextHopProtocol = item.nextHopProtocol;
//dns查询耗时
templeObj.dnsTime = item.domainLookupEnd - item.domainLookupStart;
//tcp链接耗时
templeObj.tcpTime = item.connectEnd - item.connectStart;
//请求时间
templeObj.reqTime = item.responseEnd - item.responseStart;
//重定向时间
templeObj.redirectTime = item.redirectEnd - item.redirectStart;
entryTimesList.push(templeObj);
}
});
Мы получаем массив данных, связанных с загрузкой ресурсов и асинхронными запросами через window.performance.getEntries(), а затем отфильтровываем атрибуты ['navigation','script','css' по атрибуту initiatorType каждого элемента в данные элементов массива, 'fetch', 'xmlhttprequest', 'link', 'img'].
(3), обратите внимание
-
Данные, относящиеся к рендерингу страницы, полученные с помощью window.performance.timing, не будут обновляться, если URL-адрес изменен в одностраничном приложении, но страница не обновляется. Поэтому, если вы передаете только этот API, вы не можете получить время рендеринга страницы, соответствующее каждому подмаршруту. Если вам нужно сообщить время повторного рендеринга каждой подстраницы в случае переключения маршрутов, вам нужно настроить отчет.
-
Данные, связанные с загрузкой ресурсов и асинхронными запросами, полученные через window.performance.getEntries(), будут пересчитываться при переключении маршрутов страницы, что может реализовать автоматическую отчетность.
2. Когда сообщать данные об эффективности
Затем определите, когда сообщать данные об эффективности, поскольку необходимо обрабатывать pv (посещения) и uv (посещения независимых пользователей), а обычно считается, что один отчет — это одно посещение, так когда же сообщать данные об эффективности? В моей системе я предпочитаю сообщать данные о производительности внешнего интерфейса в следующих сценариях:
- Загрузка страницы и повторное обновление
- маршрутизация переключения страниц
- Вкладка, на которой находится страница, снова становится видимой
Для трех вышеприведенных сценариев, особенно в случае переключения маршрутов, если переключение маршрута достигается путем изменения значения хэша, то необходимо отслеживать только событие hashchange. необходимо переопределить и заменить событие. Подробнее читайте в моей предыдущей статье:В одностраничном приложении, как элегантно отслеживать изменения URL-адреса.
Непосредственно давайте историю для реализации схемы мониторинга изменений url в сценариях маршрутизации:
var _wr = function(type) {
var orig = history[type];
return function() {
var rv = orig.apply(this, arguments);
var e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return rv;
};
};
history.pushState = _wr('pushState');
history.replaceState = _wr('replaceState');
Затем мы можем отслеживать соответствующие события в соответствии с приведенными выше сценариями, чтобы реализовать отчетность данных о производительности внешнего интерфейса:
addEvent(window,'load',function(e){
...deal with something
});
//监控history基础上实现的单页路由中url的变化
addEvent(window,'replaceState', function(e) {
...deal with something
});
addEvent(window,'pushState', function(e) {
...deal with something
});
//通过hash切换来实现路由的场景
addEvent(window,'hashchange',function(e){
...deal with something
});
addEvent('document','visibilitychang',function(e){
...deal with something
})
addEvent — это событие, совместимое с IE и стандартной моделью потока событий DOM.
В-третьих, как сообщать данные об эффективности
Тогда как сообщать данные о производительности, наша первая реакция — сообщать данные о производительности внешнего интерфейса в форме запросов ajax. Этот метод имеет некоторые недостатки, такие как необходимость специальной обработки для междоменного доступа, и если страница будет уничтожена, соответствующий метод ajax может не быть успешно отправлен.
Среди них кроссдоменная проблема решается проще, а самая сложная для решения проблема — второй пункт:
То есть, если страница уничтожена, соответствующий ajax-метод не может быть успешно отправлен.
Мы можем использовать различные методы для предоставления данных о производительности в зависимости от метода в Google Analytics (GA), совместимости браузера и длины URL-адреса. Основные принципы:
Благодаря динамическому созданию тегов img и объединению URL-адресов в img.src для отправки запросов нет междоменных ограничений. Если URL-адрес слишком длинный, запрос отправляется с помощью sendBeacon. Если метод sendBeacon несовместим, отправляется синхронный запрос ajax post.
(1), метод sendBeacon
Решить проблему невозможности выполнения асинхронных ajax-запросов после выгрузки документа или закрытия страницы, во многих случаях мы превратим асинхронные в синхронные. Выполните синхронный вызов метода в событии unload или beforeunload выгрузки страницы.
Однако есть проблема с синхронными вызовами методов, то есть будет задержка времени, когда страница А переключается на страницу Б. Метод sendBeacon решает эту проблему простыми словами:
Метод sendBeacon может отправлять данные асинхронно в течение периода уничтожения страницы, поэтому он не вызовет проблем с блокировкой, таких как синхронные запросы ajax, и не повлияет на отрисовку следующей страницы.
Метод вызова sendBeacon:
navigator.sendBeacon(url [, data]);
данные могут быть: ArrayBufferView, Blob, DOMString или FormData.
Чтобы отправить параметры, мы обычно формулируем данные в виде BLOB-объектов. Кроме того, следует отметить, что в заголовке запроса sendBeacon Content-Type «application/json; charset=utf-8» не поддерживается.
В заголовке sendBeacon поддерживаются только следующие три формы Content-Type:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
В общем виде сформулированный как application/x-www-form-urlencoded, полный пример отправки запроса через sendBeacon выглядит следующим образом:
function sendBeacon(url,data){
//判断支不支持navigator.sendBeacon
let headers = {
type: 'application/x-www-form-urlencoded'
};
let blob = new Blob([JSON.stringify(data)], headers);
navigator.sendBeacon(url,blob);
}
Как серверная часть обрабатывает запрос sendBeacon?SendBeacon отправляет запрос, аналогичный POST, в заголовке запроса, поэтому он может обрабатывать запрос sendBeacon аналогично постобработке.
Как правило, мы согласны с тем, что тип содержимого запроса ajax: «application/json; charset=utf-8», а тип содержимого запроса sendBeacon: «application/x-www-form-urlencoded», поэтому при внутренней обработке вы можете различить, является ли это обычным почтовым запросом ajax или запросом sendBeacon.
Кроме того, если возникает междоменная проблема при обработке запроса, ее нужно обрабатывать cors cross-domain.Бэкенд нужно настроить: разрешить-управление-разрешить-происхождение и т.д., можно упростить настройку через пакет cors экспресс:
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule,instance);
app.use(cors());
await app.listen(3000)
}
bootstrap();
(2) Динамически создавать форму тега img
Динамически создавая тег img, указывая указанный атрибутом src url для отправки запроса, во-первых, он не ограничен междоменным, а во-вторых, динамически вставляется тег img, что задержит удаление страницу и обеспечить вставку изображения, поэтому уничтожение страницы может быть гарантировано период, запрос может произойти.
Вот пример динамического создания тегов img:
function imgReport(url, data) {
if (!url || !data) {
return;
}
let image = document.createElement('img');
let items = [];
items = JSON.Parse(data);
let name = 'img_' + (+new Date());
image.onload = image.onerror = function () {
};
let newUrl = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&');
image.src = newUrl;
}
Кроме того, когда мы динамически создаем тег img для отправки запроса, запрос представляет собой изображение.При обработке бэкенда мы должны вернуть изображение в конце, чтобы метод image.onload фронтенда быть запущенным. Мы берем запрошенный адрес как: localhost:8080/1.jpg в качестве примера, логика обработки бэкэнда:
@Controller('1.jpg')
export class AppUploadController {
constructor(private readonly appService: AppService) {}
@Get()
getUpload(@Req() req,@Res() res): void {
...deal with some thing
res.sendFile(join(__dirname, '..', 'public/1.jpg'))
}
}
При обработке запроса на получение, после того как мы вернем изображение через res.sendFile(join(__dirname, '..', 'public/1.jpg')), будет вызван метод onload внешнего изображения.
(3) Синхронный почтовый запрос ajax
Метод динамического создания тегов img имеет определенные проблемы при склеивании URL-адресов, поскольку браузеры имеют ограничения на длину URL-адресов. Совместимость метода sendBeacon не очень хорошая, окончательное решение - отправить синхронный ajax-запрос.Как упоминалось ранее, синхронный ajax-запрос будет выполнен до периода уничтожения страницы, хотя и заблокирует отрисовку следующей страницы на определенной степени.
function xmlLoadData(url,data) {
var client = new XMLHttpRequest();
client.open("POST", url,false);
client.setRequestHeader("Content-Type", "application/json; charset=utf-8");
client.send(JSON.stringify(data));
}
(4) Комплексные решения
Как правило, полный URL-адрес, содержащий параметры, сначала объединяется, а длина URL-адреса оценивается. Если длина URL-адреса меньше максимальной длины, разрешенной браузером, данные о производительности внешнего интерфейса отправляются путем динамического создания img.Если URL-адрес слишком длинный, оценивается просмотр, поддерживает ли сервер метод sendBeacon, если да, отправьте запрос через метод sendBeacon, в противном случае отправьте синхронный запрос ajax.
function dealWithUrl(url,appId){
let times = performanceInfo(appId);
let items = decoupling(times);
let urlLength = (url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&')).length;
if(urlLength<2083){
imgReport(url,times);
}else if(navigator.sendBeacon){
sendBeacon(url,times);
}else{
xmlLoadData(url,times);
}
}