Une plate - forme complète de surveillance frontale comprend trois parties:Collecte et communication des données、Organisation et stockage des données、Présentation des données.
C'est la première partie de cet article——Collecte et communication des données.Voici un aperçu du contenu de cet article,Vous pouvez commencer par un aperçu de:
Il est difficile de comprendre les connaissances théoriques,Pour ce faire, j'a i écrit un simpleSurveillance SDK,Vous pouvez l'utiliser pour écrire des DEMO,Aide à approfondir la compréhension.Lisez - le avec cet article,Ça marche mieux.
Acquisition de données sur le rendement
chrome L'équipe de développement a proposé une série d'indicateurs pour mesurer le rendement des pages Web:
- FP(first-paint),Temps écoulé entre le début du chargement de la page et le premier pixel dessiné à l'écran
- FCP(first-contentful-paint),Temps écoulé entre le début du chargement de la page et la fin du rendu à l'écran de n'importe quelle partie du contenu de la page
- LCP(largest-contentful-paint),Temps écoulé entre le début du chargement de la page et la fin du rendu à l'écran du plus grand bloc de texte ou élément d'image
- CLS(layout-shift),Depuis le chargement de la page etÉtat du cycle de vieDevient le score cumulatif de tous les décalages inattendus de mise en page qui se produisent pendant la dissimulation
Ces quatre mesures de rendement doivent être adoptées. PerformanceObserver Pour obtenir(On peut aussi performance.getEntriesByName()
Accès,Mais il n'a pas été informé lorsque l'événement a été déclenché).PerformanceObserver Est un objet de surveillance du rendement,Utilisé pour surveiller les événements de mesure du rendement.
FP
FP(first-paint),Temps écoulé entre le début du chargement de la page et le premier pixel dessiné à l'écran.En fait, FP Il n'y a aucun problème à comprendre l'écran blanc..
Les codes de mesure sont les suivants::
const entryHandler = (list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-paint') {
observer.disconnect()
}
console.log(entry)
}
}
const observer = new PerformanceObserver(entryHandler)
// buffered Propriété indiquant si les données mises en cache sont observées,C'est - à - dire qu'il n'est pas important d'observer que le Code est ajouté plus tard que l'événement déclenche.
observer.observe({ type: 'paint', buffered: true })
Copier le Code
Le code ci - dessus permet d'obtenir FP Le contenu de:
{
duration: 0,
entryType: "paint",
name: "first-paint",
startTime: 359, // fp Temps
}
Copier le Code
Parmi eux startTime
C'est le temps qu'il nous faut pour dessiner..
FCP
FCP(first-contentful-paint),Temps écoulé entre le début du chargement de la page et la fin du rendu à l'écran de n'importe quelle partie du contenu de la page.Pour cet indicateur,"Contenu"Renvoie au texte、Images(Inclure une image de fond)、<svg>
Element or not White<canvas>
Élément.
Pour offrir une bonne expérience utilisateur,FCP Le score de 1.8 En quelques secondes.
Code de mesure:
const entryHandler = (list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
observer.disconnect()
}
console.log(entry)
}
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'paint', buffered: true })
Copier le Code
Le code ci - dessus permet d'obtenir FCP Le contenu de:
{
duration: 0,
entryType: "paint",
name: "first-contentful-paint",
startTime: 459, // fcp Temps
}
Copier le Code
Parmi eux startTime
C'est le temps qu'il nous faut pour dessiner..
LCP
LCP(largest-contentful-paint),Temps écoulé entre le début du chargement de la page et la fin du rendu à l'écran du plus grand bloc de texte ou élément d'image.LCP L'indicateur sera basé sur la pageChargement initialPoint dans le temps pour signaler le maximum visible dans la zone visibleBloc d'image ou de texteTemps relatif pour terminer le rendu.
Un bon LCP Les scores devraient être contrôlés 2.5 En quelques secondes.
Code de mesure:
const entryHandler = (list) => {
if (observer) {
observer.disconnect()
}
for (const entry of list.getEntries()) {
console.log(entry)
}
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'largest-contentful-paint', buffered: true })
Copier le Code
Le code ci - dessus permet d'obtenir LCP Le contenu de:
{
duration: 0,
element: p,
entryType: "largest-contentful-paint",
id: "",
loadTime: 0,
name: "",
renderTime: 1021.299,
size: 37932,
startTime: 1021.299,
url: "",
}
Copier le Code
Parmi eux startTime
C'est le temps qu'il nous faut pour dessiner..element Oui. LCP Dessiné DOM Élément.
FCP Et LCP La différence entre:FCP Déclenché chaque fois que n'importe quel contenu est dessiné,LCP Est déclenché lorsque le rendu de contenu maximum est terminé.
LCP Les types d'éléments examinés sont::
<img>
Élément- Intégré dans
<svg>
Dans l'élément<image>
Élément <video>
Élément(Utiliser l'image de couverture)- Adoption
url()
Fonctions(Plutôt que d'utiliserCSS Gradient)Éléments chargés avec image de fond - Contenant des noeuds de texte ou d'autres sous - éléments de texte au niveau de la ligneÉléments au niveau du bloc.
CLS
CLS(layout-shift),Depuis le chargement de la page etÉtat du cycle de vieDevient le score cumulatif de tous les décalages inattendus de mise en page qui se produisent pendant la dissimulation.
La fraction de décalage de disposition est calculée comme suit::
Score offset de mise en page = Score d'impact * Fraction de distance
Copier le Code
Score d'impactMesureÉlément instable.Effet sur la zone de visualisation entre deux images.
Fraction de distanceSe réfère à n'importe quelÉlément instableDistance maximale de déplacement en un seul cadre(Horizontal ou vertical)Diviser par la dimension maximale de la zone visible(Largeur ou hauteur,Le plus élevé des montants suivants est retenu:).
CLS C'est la somme de tous les décalages de mise en page.
Quand un DOM Déplacement entre les deux cadres rendus,Ça va déclencher CLS(Comme le montre la figure).
Le rectangle de l'image ci - dessus se déplace du coin supérieur gauche vers la droite,C'est un décalage de mise en page..En même temps,In CLS Moyenne,Il y en a un qui s'appelleFenêtre de sessionTerminologie:.Un ou plusieurs décalages de mise en page uniques qui se produisent rapidement et consécutivement,Chaque décalage est inférieur à 1 Secondes,Et la durée maximale de toute la fenêtre est 5 Secondes.
Par exemple, la deuxième fenêtre de session de l'image ci - dessus,Il y a quatre décalages de disposition à l'intérieur.,L'intervalle entre chaque décalage doit être inférieur à 1 Secondes,Et le temps entre le premier et le dernier décalage ne doit pas dépasser 5 Secondes,C'est une fenêtre de session..Si cette condition n'est pas remplie,Même une nouvelle fenêtre de session.Quelqu'un pourrait demander,Pourquoi cette règle??En fait, c'est chrome Résultats de l'analyse de l'équipe à partir d'un grand nombre d'expériences et d'études Evolving the CLS metric.
CLS Il y a trois façons de calculer:
- Cumul
- Moyenne de toutes les fenêtres de session
- Prend la valeur maximale dans toutes les fenêtres de session
Cumul
C'est - à - dire additionner tous les points de décalage de mise en page à partir du chargement de la page.Mais ce calcul n'est pas convivial pour les pages à longue durée de vie,Plus la page reste longtemps,CLS Plus le score est élevé.
Moyenne de toutes les fenêtres de session
Ce calcul n'est pas effectué en unités décalées par une seule disposition,Au lieu de cela, la fenêtre de session.Additionnez les valeurs de toutes les fenêtres de session et faites la moyenne.Mais cette méthode de calcul présente également des inconvénients..
Comme vous pouvez le voir sur la photo ci - dessus,La première fenêtre de session a produit CLS Points,La deuxième fenêtre de session a produit CLS Points.Si vous prenez leur moyenne comme CLS Points,Vous ne pouvez pas voir la santé de la page.Il s'avère que la page a été décalée plus tôt,Moins de décalage à un stade ultérieur,La moyenne actuelle ne reflète pas cette situation.
Prend la valeur maximale dans toutes les fenêtres de session
C'est la meilleure façon de calculer.,Prend le maximum de toutes les fenêtres de session à la fois,Utilisé pour refléter le pire cas de décalage de mise en page.Pour plus de détails, voir Evolving the CLS metric.
Voici les codes de mesure pour la troisième méthode de calcul:
let sessionValue = 0
let sessionEntries = []
const cls = {
subType: 'layout-shift',
name: 'layout-shift',
type: 'performance',
pageURL: getPageURL(),
value: 0,
}
const entryHandler = (list) => {
for (const entry of list.getEntries()) {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0]
const lastSessionEntry = sessionEntries[sessionEntries.length - 1]
// If the entry occurred less than 1 second after the previous entry and
// less than 5 seconds after the first entry in the session, include the
// entry in the current session. Otherwise, start a new session.
if (
sessionValue
&& entry.startTime - lastSessionEntry.startTime < 1000
&& entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value
sessionEntries.push(formatCLSEntry(entry))
} else {
sessionValue = entry.value
sessionEntries = [formatCLSEntry(entry)]
}
// If the current session value is larger than the current CLS value,
// update CLS and the entries contributing to it.
if (sessionValue > cls.value) {
cls.value = sessionValue
cls.entries = sessionEntries
cls.startTime = performance.now()
lazyReportCache(deepCopy(cls))
}
}
}
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'layout-shift', buffered: true })
Copier le Code
Après avoir lu la description textuelle ci - dessus,Regardez le Code et vous comprendrez..La mesure du décalage de disposition primaire est la suivante::
{
duration: 0,
entryType: "layout-shift",
hadRecentInput: false,
lastInputTime: 0,
name: "",
sources: (2) [LayoutShiftAttribution, LayoutShiftAttribution],
startTime: 1176.199999999255,
value: 0.000005752046026677329,
}
Copier le Code
Dans le Code value
Le champ est le score offset de mise en page.
DOMContentLoaded、load Événements
Quand pur HTML Lorsqu'il est entièrement chargé et analysé,DOMContentLoaded
L'événement sera déclenché,Pas besoin d'attendre css、img、iframe Chargement terminé.
Lorsque la page entière et toutes les ressources dépendantes telles que les feuilles de style et les images sont chargées,Déclenchera load
Événements.
Bien que ces deux indicateurs de rendement soient plus anciens,Mais ils peuvent encore refléter certaines situations sur la page.Il est toujours nécessaire de les surveiller..
import { lazyReportCache } from '../utils/report'
['load', 'DOMContentLoaded'].forEach(type => onEvent(type))
function onEvent(type) {
function callback() {
lazyReportCache({
type: 'performance',
subType: type.toLocaleLowerCase(),
startTime: performance.now(),
})
window.removeEventListener(type, callback, true)
}
window.addEventListener(type, callback, true)
}
Copier le Code
Temps de rendu du premier écran
Dans la plupart des cas,Le temps de rendu du premier écran peut être atteint en load
Acquisition d'événements.Sauf dans des circonstances exceptionnelles.,Par exemple, les images chargées asynchrones et DOM.
<script> setTimeout(() => { document.body.innerHTML = ` <div> <!-- Omettre un tas de code... --> </div> ` }, 3000) </script>
Copier le Code
Ça ne passera pas. load
L'événement a le temps de rendu du premier écran.Nous devons passer MutationObserver Pour obtenir le temps de rendu du premier écran.MutationObserver On écoute. DOM Événements déclenchés lorsque les attributs de l'élément changent.
Processus de calcul du temps de rendu du premier écran:
- Utilisation MutationObserver Écouter document Objet,Chaque fois que DOM Lorsque les attributs de l'élément changent,Déclencheur d'événements.
- Juge ça. DOM Si l'élément est dans le premier écran,Si dans,Dans
requestAnimationFrame()
Appelé dans une fonction de rappelperformance.now()
Obtenir l'heure actuelle,Comme son temps de dessin. - Le dernier DOM Le temps de dessin de l'élément est comparé au temps de toutes les images chargées dans le premier écran,Utiliser le maximum comme temps de rendu du premier écran.
Écouter DOM
const next = window.requestAnimationFrame ? requestAnimationFrame : setTimeout
const ignoreDOMList = ['STYLE', 'SCRIPT', 'LINK']
observer = new MutationObserver(mutationList => {
const entry = {
children: [],
}
for (const mutation of mutationList) {
if (mutation.addedNodes.length && isInScreen(mutation.target)) {
// ...
}
}
if (entry.children.length) {
entries.push(entry)
next(() => {
entry.startTime = performance.now()
})
}
})
observer.observe(document, {
childList: true,
subtree: true,
})
Copier le Code
Le code ci - dessus est Listener DOM Codes modifiés,Doit être filtré en même temps style
、script
、link
Étiquette isométrique.
Déterminer s'il est sur le premier écran
Une page peut avoir beaucoup de contenu,Mais l'utilisateur ne peut voir que le contenu d'un seul écran.Donc en comptant le temps de rendu du premier écran,,Champ d'application requis,Limiter le contenu rendu à l'écran courant.
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
// dom Si l'objet est à l'écran
function isInScreen(dom) {
const rectInfo = dom.getBoundingClientRect()
if (rectInfo.left < viewportWidth && rectInfo.top < viewportHeight) {
return true
}
return false
}
Copier le Code
Utiliser requestAnimationFrame()
Accès DOM Temps de tirage
Quand DOM Changement déclenché MutationObserver Au moment de l'événement,Juste un représentant. DOM Le contenu peut être lu à,Ça ne veut pas dire que DOM Est dessiné à l'écran.
Comme le montre la figure ci - dessus,Quand il est déclenché MutationObserver Au moment de l'événement,Peut être lu à document.body
Il y a déjà quelque chose dessus.,Mais en fait, l'écran de gauche n'a rien dessiné.Alors appelez requestAnimationFrame()
Obtenez l'heure actuelle comme DOM Temps de tirage.
Comparé au temps de chargement de toutes les images sur le premier écran
function getRenderTime() {
let startTime = 0
entries.forEach(entry => {
if (entry.startTime > startTime) {
startTime = entry.startTime
}
})
// Besoin de comparer avec le temps de chargement de toutes les images de la page courante,Prendre la valeur maximale
// Le temps de demande d'image doit être inférieur à startTime,Le temps de fin de réponse doit être supérieur à startTime
performance.getEntriesByType('resource').forEach(item => {
if (
item.initiatorType === 'img'
&& item.fetchStart < startTime
&& item.responseEnd > startTime
) {
startTime = item.responseEnd
}
})
return startTime
}
Copier le Code
Optimisation
Le Code n'est pas encore optimisé.,Deux considérations principales:
- Quand déclarer le temps de rendu?
- Si compatible avec l'ajout asynchrone DOM Situation?
Premier point,Doit être DOM Ne changez pas avant de déclarer le temps de rendu,En général load Après le déclenchement de l'événement,DOM Ça ne change plus..Pour qu'on puisse faire un rapport à ce moment - là.
Deuxième point,Ça pourrait être dans LCP Signaler après le déclenchement de l'événement.Chargement synchrone ou asynchrone DOM,Il faut le dessiner.,Pour pouvoir écouter LCP Événements,L'escalade n'est autorisée qu'après le déclenchement de l'événement.
Combiner les deux solutions ci - dessus,Avec le code suivant:
let isOnLoaded = false
executeAfterLoad(() => {
isOnLoaded = true
})
let timer
let observer
function checkDOMChange() {
clearTimeout(timer)
timer = setTimeout(() => {
// Attendez. load、lcp Après le déclenchement de l'événement et DOM Quand l'arbre ne change plus,Calculer le temps de rendu du premier écran
if (isOnLoaded && isLCPDone()) {
observer && observer.disconnect()
lazyReportCache({
type: 'performance',
subType: 'first-screen-paint',
startTime: getRenderTime(),
pageURL: getPageURL(),
})
entries = null
} else {
checkDOMChange()
}
}, 500)
}
Copier le Code
checkDOMChange()
Le Code est déclenché à chaque fois MutationObserver Appelé à l'événement,Doit être traité avec une fonction anti - bavardage.
La demande d'interface prend du temps
La demande d'interface prend du temps XMLHttpRequest Et fetch Pour écouter.
Écouter XMLHttpRequest
originalProto.open = function newOpen(...args) {
this.url = args[1]
this.method = args[0]
originalOpen.apply(this, args)
}
originalProto.send = function newSend(...args) {
this.startTime = Date.now()
const onLoadend = () => {
this.endTime = Date.now()
this.duration = this.endTime - this.startTime
const { status, duration, startTime, endTime, url, method } = this
const reportData = {
status,
duration,
startTime,
endTime,
url,
method: (method || 'GET').toUpperCase(),
success: status >= 200 && status < 300,
subType: 'xhr',
type: 'performance',
}
lazyReportCache(reportData)
this.removeEventListener('loadend', onLoadend, true)
}
this.addEventListener('loadend', onLoadend, true)
originalSend.apply(this, args)
}
Copier le Code
Comment juger XML La demande a - t - elle été acceptée??Selon que son code d'état est 200~299 Entre.Si dans,C'est le succès,Sinon, l'échec.
Écouter fetch
const originalFetch = window.fetch
function overwriteFetch() {
window.fetch = function newFetch(url, config) {
const startTime = Date.now()
const reportData = {
startTime,
url,
method: (config?.method || 'GET').toUpperCase(),
subType: 'fetch',
type: 'performance',
}
return originalFetch(url, config)
.then(res => {
reportData.endTime = Date.now()
reportData.duration = reportData.endTime - reportData.startTime
const data = res.clone()
reportData.status = data.status
reportData.success = data.ok
lazyReportCache(reportData)
return res
})
.catch(err => {
reportData.endTime = Date.now()
reportData.duration = reportData.endTime - reportData.startTime
reportData.status = 0
reportData.success = false
lazyReportCache(reportData)
throw err
})
}
}
Copier le Code
Pour fetch,Peut être basé sur ok
Champ pour déterminer si la demande a été acceptée,Si oui true
Demande acceptée,Sinon, l'échec.
Attention!,Temps de demande d'interface écouté et chrome devtool Le temps détecté sur peut être différent.C'est parce que chrome devtool Ce qui a été détecté sur HTTP Temps d'envoi de la demande et d'interface tout au long du processus.Mais xhr Et fetch Est une requête asynchrone,La fonction de rappel doit être appelée après une demande d'interface réussie.L'événement déclenche la fonction de rappel dans la file d'attente des messages,Puis le navigateur traite,Il y a aussi un processus d'attente.
Temps de chargement des ressources、Taux de succès du cache
Adoption PerformanceObserver
On peut écouter. resource
Et navigation
Événements,Si le navigateur ne supporte pas PerformanceObserver
,Peut encore passer performance.getEntriesByType(entryType)
Pour le déclassement.
Quand resource
Lorsque l'événement est déclenché,Vous pouvez obtenir la liste des ressources correspondantes,Chaque objet de ressource contient les champs suivants::
Nous pouvons extraire des informations utiles de ces champs:
{
name: entry.name, // Nom de la ressource
subType: entryType,
type: 'performance',
sourceType: entry.initiatorType, // Type de ressource
duration: entry.duration, // Le chargement des ressources prend du temps
dns: entry.domainLookupEnd - entry.domainLookupStart, // DNS Ça prend du temps
tcp: entry.connectEnd - entry.connectStart, // Établissement tcp Connexion longue
redirect: entry.redirectEnd - entry.redirectStart, // La redirection prend du temps
ttfb: entry.responseStart, // Temps du premier octet
protocol: entry.nextHopProtocol, // Demande d'accord
responseBodySize: entry.encodedBodySize, // Taille du contenu de la réponse
responseHeaderSize: entry.transferSize - entry.encodedBodySize, // Taille de la tête de réponse
resourceSize: entry.decodedBodySize, // Taille de la ressource après décompression
isCache: isCache(entry), // Si le cache est touché
startTime: performance.now(),
}
Copier le Code
Déterminer si la ressource a touché le cache
L'un de ces objets de ressources transferSize
Champ,Il représente la taille de la ressource acquise,Inclure la taille des champs d'en - tête de réponse et des données de réponse.Si cette valeur est 0,La description est lue directement à partir du cache(Mise en cache forcée).Si cette valeur n'est pas 0,Mais encodedBodySize
Le champ est 0,Indique qu'il prend le cache de négociation(encodedBodySize
Représente les données de réponse à la demande body Taille).
function isCache(entry) {
// Lire directement à partir du cache ou 304
return entry.transferSize === 0 || (entry.transferSize !== 0 && entry.encodedBodySize === 0)
}
Copier le Code
Non - respect des conditions ci - dessus,Description cache manquant.Et ensuite,Toutes les données qui ont touché le cache/Total des données
Pour obtenir le taux de succès du cache.
Cache aller - retour du Navigateur BFC(back/forward cache)
bfcache Est un cache de mémoire,Il garde toute la page en mémoire.La page entière est immédiatement visible lorsque l'utilisateur revient,Au lieu de rafraîchir à nouveau.Selon cet article bfcache Introduction,firfox Et safari Toujours soutenu bfc,chrome Prise en charge uniquement sur les navigateurs mobiles haute version.Mais j'ai essayé.,Seulement safari Prise en charge du Navigateur,Peut - être le mien. firfox Mauvaise version.
Mais bfc Il y a aussi des défauts.,Lorsque l'utilisateur retourne à bfc Lors de la restauration d'une page,Le Code de la page originale ne sera pas exécuté à nouveau.À cette fin,,Le navigateur fournit un pageshow
Événements,Vous pouvez y mettre le Code qui doit être exécuté à nouveau.
window.addEventListener('pageshow', function(event) {
// Si la propriété est true,Indique que c'est à partir de bfc Pages récupérées dans
if (event.persisted) {
console.log('This page was restored from the bfcache.');
} else {
console.log('This page was loaded normally.');
}
});
Copier le Code
De bfc Pages récupérées dans,Nous devons aussi collecter leurs FP、FCP、LCP Attendre toutes sortes de temps.
onBFCacheRestore(event => {
requestAnimationFrame(() => {
['first-paint', 'first-contentful-paint'].forEach(type => {
lazyReportCache({
startTime: performance.now() - event.timeStamp,
name: type,
subType: type,
type: 'performance',
pageURL: getPageURL(),
bfc: true,
})
})
})
})
Copier le Code
Le code ci - dessus est bien compris,In pageshow
Après le déclenchement de l'événement,Soustraire le temps de déclenchement de l'événement de l'heure actuelle,Cette différence de temps est le temps de dessin de l'indice de performance.Attention!,De bfc Ces mesures de performance pour les pages récupérées dans,Les valeurs sont généralement faibles,En général 10 ms Gauche et droite.Donc vous leur donnez un champ d'identification bfc: true
.Cela permet de les ignorer dans les statistiques de performance.
FPS
Utilisation requestAnimationFrame()
Nous pouvons calculer FPS.
const next = window.requestAnimationFrame
? requestAnimationFrame : (callback) => { setTimeout(callback, 1000 / 60) }
const frames = []
export default function fps() {
let frame = 0
let lastSecond = Date.now()
function calculateFPS() {
frame++
const now = Date.now()
if (lastSecond + 1000 <= now) {
// Parce que now - lastSecond En millisecondes,Alors... frame Oui. * 1000
const fps = Math.round((frame * 1000) / (now - lastSecond))
frames.push(fps)
frame = 0
lastSecond = now
}
// Évitez de signaler trop rapidement,Mettre en cache un certain nombre de rapports
if (frames.length >= 60) {
report(deepCopy({
frames,
type: 'performace',
subType: 'fps',
}))
frames.length = 0
}
next(calculateFPS)
}
calculateFPS()
}
Copier le Code
La logique du Code est la suivante::
- Enregistrer une heure initiale,Et chaque fois que ça se déclenche,
requestAnimationFrame()
Heure,Ajoutez juste le nombre de cadres 1.Dans une seconde.Nombre de cadres/Temps écoulé
Pour obtenir le taux de trame actuel.
Lorsque trois consécutifs sont inférieurs à 20 De FPS Au moment de l'apparition,On peut conclure que la page est carton.,Pour plus de détails, voir Comment surveiller les captures de page.
export function isBlocking(fpsList, below = 20, last = 3) {
let count = 0
for (let i = 0; i < fpsList.length; i++) {
if (fpsList[i] && fpsList[i] < below) {
count++
} else {
count = 0
}
if (count >= last) {
return true
}
}
return false
}
Copier le Code
Vue Temps de rendu du changement de routage
Nous savons déjà comment calculer le temps de rendu du premier écran,Mais comment calculer SPA Qu'en est - il du temps de rendu de page causé par le routage de page appliqué?Pour cet article Vue Par exemple,Dis - moi ce que je pense..
export default function onVueRouter(Vue, router) {
let isFirst = true
let startTime
router.beforeEach((to, from, next) => {
// D'autres statistiques de temps de rendu sont déjà disponibles pour la première fois sur la page d'entrée
if (isFirst) {
isFirst = false
return next()
}
// Voilà. router Ajouter un nouveau champ,Indique si le temps de rendu doit être calculé
// Seuls les sauts de routage doivent être calculés
router.needCalculateRenderTime = true
startTime = performance.now()
next()
})
let timer
Vue.mixin({
mounted() {
if (!router.needCalculateRenderTime) return
this.$nextTick(() => {
// .Code qui ne s'exécute qu'après que la vue entière a été rendue
const now = performance.now()
clearTimeout(timer)
timer = setTimeout(() => {
router.needCalculateRenderTime = false
lazyReportCache({
type: 'performance',
subType: 'vue-router-change-paint',
duration: now - startTime,
startTime: now,
pageURL: getPageURL(),
})
}, 1000)
})
},
})
}
Copier le Code
La logique du Code est la suivante::
- Écouter les crochets de routage,Déclenché lors du changement de route
router.beforeEach()
Crochet,Dans la fonction de rappel de ce crochet, Notez l'heure actuelle comme l'heure de début du rendu. - Utilisation
Vue.mixin()
Pour tous les composantsmounted()
Injecter une fonction.Chaque fonction exécute une fonction tampon. - Quand le dernier composant
mounted()
Quand ça se déclenche,Cela signifie que tous les composants sous cette route ont été montés.Ça pourrait être dansthis.$nextTick()
Obtenir le temps de rendu dans la fonction de rappel.
En même temps,Il faut aussi tenir compte d'une situation.Lorsque le routage n'est pas commuté,Il peut également y avoir des changements dans les composants,Ne devrait pas être dans ces composants pour le moment mounted()
Calculer le temps de rendu.Donc vous devez ajouter un needCalculateRenderTime
Champ,Définir le routage comme true,Le représentant peut calculer le temps de rendu.
Collecte de données d'erreur
Erreur de chargement de la ressource
Utiliser addEventListener()
Écouter error Événements,Les erreurs de chargement des ressources peuvent être saisies.
// Erreur de chargement de la ressource de capture js css img...
window.addEventListener('error', e => {
const target = e.target
if (!target) return
if (target.src || target.href) {
const url = target.src || target.href
lazyReportCache({
url,
type: 'error',
subType: 'resource',
startTime: e.timeStamp,
html: target.outerHTML,
resourceType: target.tagName,
paths: e.path.map(item => item.tagName).filter(Boolean),
pageURL: getPageURL(),
})
}
}, true)
Copier le Code
js Erreur
Utiliser window.onerror
On peut écouter. js Erreur.
// Écouter js Erreur
window.onerror = (msg, url, line, column, error) => {
lazyReportCache({
msg,
line,
column,
error: error.stack,
subType: 'js',
pageURL: url,
type: 'error',
startTime: performance.now(),
})
}
Copier le Code
promise Erreur
Utiliser addEventListener()
Écouter unhandledrejection Événements,Peut capturer non géré promise Erreur.
// Écouter promise Erreur L'inconvénient est que les données de colonne ne sont pas disponibles
window.addEventListener('unhandledrejection', e => {
lazyReportCache({
reason: e.reason?.stack,
subType: 'promise',
type: 'error',
startTime: e.timeStamp,
pageURL: getPageURL(),
})
})
Copier le Code
sourcemap
En général, le Code de l'environnement de production est compressé,Et l'environnement de production ne met pas sourcemap Téléchargement de fichiers.Il est donc difficile de lire les informations d'erreur de code dans l'environnement de production.Donc,,Nous pouvons utiliser source-map Pour restaurer ces messages d'erreur de code compressés.
Lorsque le Code signale une erreur,Nous pouvons obtenir le nom de fichier correspondant、Nombre de lignes、Nombre de colonnes:
{
line: 1,
column: 17,
file: 'https:/www.xxx.com/bundlejs',
}
Copier le Code
Puis appelez le code suivant pour restaurer:
async function parse(error) {
const mapObj = JSON.parse(getMapFileContent(error.url))
const consumer = await new sourceMap.SourceMapConsumer(mapObj)
// Oui. webpack://source-map-demo/./src/index.js Dans le fichier ./ Enlevez
const sources = mapObj.sources.map(item => format(item))
// Le nombre de lignes d'erreur et de fichiers source non compressés est obtenu à partir des informations d'erreur compressées
const originalInfo = consumer.originalPositionFor({ line: error.line, column: error.column })
// sourcesContent Contient le code source non compressé de chaque fichier,Trouver le code source correspondant en fonction du nom du fichier
const originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
return {
file: originalInfo.source,
content: originalFileContent,
line: originalInfo.line,
column: originalInfo.column,
msg: error.msg,
error: error.error
}
}
function format(item) {
return item.replace(/(\.\/)*/g, '')
}
function getMapFileContent(url) {
return fs.readFileSync(path.resolve(__dirname, `./maps/${url.split('/').pop()}.map`), 'utf-8')
}
Copier le Code
Chaque fois que le projet est emballé,Si elle est allumée sourcemap,Donc chaque js Tous les fichiers auront une correspondance map Documentation.
bundle.js
bundle.js.map
Copier le Code
À ce moment - là, js Les fichiers sont placés sur un serveur statique pour l'accès de l'utilisateur,map Les fichiers sont stockés sur le serveur,Utilisé pour restaurer les messages d'erreur.source-map
.La bibliothèque peut restaurer les messages d'erreur de code non compressés basés sur les messages d'erreur de code compressés.Par exemple, la position d'erreur après compression est 1 D'accord 47 Colonnes
,L'emplacement réel après restauration peut être 4 D'accord 10 Colonnes
.En plus des informations de localisation,Le texte original du code source est également disponible.
La figure ci - dessus est un exemple de code qui a été restauré par erreur.Considérant que cette partie ne relève pas SDK Champ d'application,Alors j'en ai un autre. Entrepôt Pour faire ça.,Si vous êtes intéressé, vous pouvez voir.
Vue Erreur
Utilisation window.onerror
C'est impossible à capturer. Vue Faux.,Il faut l'utiliser Vue Fourni API Pour écouter.
Vue.config.errorHandler = (err, vm, info) => {
// Imprimer les messages d'erreur sur la console
console.error(err)
lazyReportCache({
info,
error: err.stack,
subType: 'vue',
type: 'error',
startTime: performance.now(),
pageURL: getPageURL(),
})
}
Copier le Code
Acquisition de données comportementales
PV、UV
PV(page view) Est le nombre de pages vues,UV(Unique visitor)Accès des utilisateurs.PV Il suffit de visiter la page une fois.,UV Plusieurs visites au cours de la même journée ne sont comptées qu'une seule fois.
Pour le Front End,Chaque fois que vous accédez à la page PV C'est tout.,UV Les statistiques sont mises sur le serveur,Il s'agit principalement d'analyser les données déclarées pour les statistiques UV.
export default function pv() {
lazyReportCache({
type: 'behavior',
subType: 'pv',
startTime: performance.now(),
pageURL: getPageURL(),
referrer: document.referrer,
uuid: getUUID(),
})
}
Copier le Code
Durée du séjour de la page
L'utilisateur entre dans la page pour enregistrer une heure initiale,Soustraire l'heure initiale de l'heure courante lorsque l'utilisateur quitte la page,Est la durée du séjour de l'utilisateur.Cette logique de calcul peut être placée dans beforeunload
Dans l'incident.
export default function pageAccessDuration() {
onBeforeunload(() => {
report({
type: 'behavior',
subType: 'page-access-duration',
startTime: performance.now(),
pageURL: getPageURL(),
uuid: getUUID(),
}, true)
})
}
Copier le Code
Profondeur d'accès à la page
Il est utile d'enregistrer la profondeur d'accès à la page,Par exemple, différentes pages actives a Et b.a Profondeur moyenne d'accès seulement 50%,b La profondeur moyenne d'accès est de 80%,Description b Plus populaire auprès des utilisateurs,Des modifications ciblées peuvent être apportées en fonction de ce point. a Page active.
En outre, la profondeur d'accès et la durée de séjour peuvent être utilisées pour identifier les brosses de commerce électronique.Par exemple, quelqu'un entre dans la page et tire la page en bas et attend un certain temps avant d'acheter,Quelqu'un fait défiler la page lentement.,Achat final.Bien qu'ils restent sur la page pendant la même période,Mais apparemment, la première personne est plus comme une brosse à linge..
Le processus de calcul de la profondeur d'accès à la page est un peu plus compliqué:
- Lorsque l'utilisateur entre dans la page,Enregistrer l'heure actuelle、scrollTop Valeur、Hauteur visuelle de la page、Hauteur totale de la page.
- Le moment où l'utilisateur fait défiler la page,Ça va déclencher
scroll
Événements,.Calculer la profondeur d'accès à la page et la durée du séjour en utilisant les données obtenues au premier point dans la fonction de rappel. - Lorsque l'utilisateur fait défiler la page vers un point,Arrêtez - vous et continuez à regarder la page.L'heure actuelle est enregistrée、scrollTop Valeur、Hauteur visuelle de la page、Hauteur totale de la page.
- Répétez le deuxième point....
Voir le code spécifique:
let timer
let startTime = 0
let hasReport = false
let pageHeight = 0
let scrollTop = 0
let viewportHeight = 0
export default function pageAccessHeight() {
window.addEventListener('scroll', onScroll)
onBeforeunload(() => {
const now = performance.now()
report({
startTime: now,
duration: now - startTime,
type: 'behavior',
subType: 'page-access-height',
pageURL: getPageURL(),
value: toPercent((scrollTop + viewportHeight) / pageHeight),
uuid: getUUID(),
}, true)
})
// Initialiser l'enregistrement après le chargement de la page hauteur d'accès actuelle、Temps
executeAfterLoad(() => {
startTime = performance.now()
pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight
scrollTop = document.documentElement.scrollTop || document.body.scrollTop
viewportHeight = window.innerHeight
})
}
function onScroll() {
clearTimeout(timer)
const now = performance.now()
if (!hasReport) {
hasReport = true
lazyReportCache({
startTime: now,
duration: now - startTime,
type: 'behavior',
subType: 'page-access-height',
pageURL: getPageURL(),
value: toPercent((scrollTop + viewportHeight) / pageHeight),
uuid: getUUID(),
})
}
timer = setTimeout(() => {
hasReport = false
startTime = now
pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight
scrollTop = document.documentElement.scrollTop || document.body.scrollTop
viewportHeight = window.innerHeight
}, 500)
}
function toPercent(val) {
if (val >= 1) return '100%'
return (val * 100).toFixed(2) + '%'
}
Copier le Code
Cliquez sur
Utilisation addEventListener()
Écouter mousedown
、touchstart
Événements,Nous pouvons recueillir la taille de chaque zone cliquée par l'utilisateur,.Cliquez sur l'emplacement exact des coordonnées sur toute la page,Cliquez sur le contenu de l'élément et d'autres informations.
export default function onClick() {
['mousedown', 'touchstart'].forEach(eventType => {
let timer
window.addEventListener(eventType, event => {
clearTimeout(timer)
timer = setTimeout(() => {
const target = event.target
const { top, left } = target.getBoundingClientRect()
lazyReportCache({
top,
left,
eventType,
pageHeight: document.documentElement.scrollHeight || document.body.scrollHeight,
scrollTop: document.documentElement.scrollTop || document.body.scrollTop,
type: 'behavior',
subType: 'click',
target: target.tagName,
paths: event.path?.map(item => item.tagName).filter(Boolean),
startTime: event.timeStamp,
pageURL: getPageURL(),
outerHTML: target.outerHTML,
innerHTML: target.innerHTML,
width: target.offsetWidth,
height: target.offsetHeight,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
uuid: getUUID(),
})
}, 500)
})
})
}
Copier le Code
Saut de page
Utilisation addEventListener()
Écouter popstate
、hashchange
Événement de saut de page.Notez que l'appelhistory.pushState()
Ouhistory.replaceState()
Ne pas déclencherpopstate
Événements.Seulement si vous faites une action de navigateur,L'événement ne sera déclenché que,Si l'utilisateur clique sur le bouton arrière du Navigateur(OuJavascriptAppelé en Codehistory.back()
Ouhistory.forward()
Méthodes).Même chose.,hashchange
C'est pareil..
export default function pageChange() {
let from = ''
window.addEventListener('popstate', () => {
const to = getPageURL()
lazyReportCache({
from,
to,
type: 'behavior',
subType: 'popstate',
startTime: performance.now(),
uuid: getUUID(),
})
from = to
}, true)
let oldURL = ''
window.addEventListener('hashchange', event => {
const newURL = event.newURL
lazyReportCache({
from: oldURL,
to: newURL,
type: 'behavior',
subType: 'hashchange',
startTime: performance.now(),
uuid: getUUID(),
})
oldURL = newURL
}, true)
}
Copier le Code
Vue Changement de route
Vue Peut être utilisé router.beforeEach
Crochet pour écouter les changements de routage.
export default function onVueRouter(router) {
router.beforeEach((to, from, next) => {
// La première page de chargement n'a pas besoin de statistiques
if (!from.name) {
return next()
}
const data = {
params: to.params,
query: to.query,
}
lazyReportCache({
data,
name: to.name || to.path,
type: 'behavior',
subType: ['vue-router-change', 'pv'],
startTime: performance.now(),
from: from.fullPath,
to: to.fullPath,
uuid: getUUID(),
})
next()
})
}
Copier le Code
Communication des données
Méthode d & apos; établissement des rapports
La Déclaration des données peut être effectuée de plusieurs façons::
- sendBeacon
- XMLHttpRequest
- image
La simplicité de mon écriture SDK C'est le premier.、La deuxième méthode combine la méthode de déclaration.Utilisation sendBeacon Les avantages de l'escalade sont évidents.
Utiliser
sendBeacon()
La méthode permet à l'agent utilisateur d'envoyer des données asynchrones au serveur lorsqu'il en a l'occasion,Sans retarder le déchargement de la page ou affecter les performances de chargement de la prochaine navigation.Cela résout tous les problèmes liés à la présentation des données analytiques:Fiabilité des données,Le transfert est asynchrone et n'affecte pas le chargement de la page suivante.
Non pris en charge sendBeacon Sous le navigateur, nous pouvons utiliser XMLHttpRequest Pour l'escalade.Un HTTP La demande comprend deux étapes d'envoi et de réception.En fait, pour l'escalade,,On doit juste s'assurer que ça sort..C'est - à - dire que l'envoi est réussi.,Peu importe si la réponse est reçue ou non..À cette fin,,J'ai fait une expérience.,In beforeunload Avec XMLHttpRequest C'est transmis. 30kb Données(Les données générales à déclarer sont rarement aussi volumineuses),Changement de navigateur,Peut être envoyé avec succès.Bien sûr.,Ceci et les performances matérielles、L'état du réseau est également pertinent.
Calendrier des rapports
Il y a trois possibilités de déclaration:
- Adoption
requestIdleCallback/setTimeout
Report du délai. - In beforeunload Rapport dans la fonction de rappel.
- Mise en cache des données d'escalade,Une fois qu'une certaine quantité est atteinte, elle doit être signalée..
Il est recommandé de combiner les trois méthodes d'escalade.:
- Mettre en cache d'abord les données d'escalade,Après avoir mis en cache un certain nombre,Utilisation
requestIdleCallback/setTimeout
Report du délai. - Soumettre uniformément les données non déclarées au départ de la page.
Résumé
Il est difficile de comprendre les connaissances théoriques,Pour ce faire, j'ai écrit un simpleSurveillance SDK,Vous pouvez l'utiliser pour écrire des DEMO,Aide à approfondir la compréhension.Lisez - le avec cet article,Ça marche mieux.
Si vous pensez que cet article vous aidera un peu,Fais - moi plaisir..Ou peut - être rejoindre mon groupe de développement:1025263163Apprendre les uns des autres,Nous aurons des réponses techniques professionnelles
Si vous trouvez cet article utile,S'il vous plaît, donnez - nous un peu de notre projet Open Sourcestar: http://github.crmeb.net/u/defu Merci beaucoup. !