Why monitor

Use (on) door (emperor) say, this page how so slow, still have no one tube? !

In short, there are three reasons:

  • It is the nature of engineers to focus on performance.
  • Page performance is critical to the user experience. For the improvement of page performance by each refactoring, it is not convincing to rely only on the test data of engineers’ development equipment, and a large amount of real data is needed for verification;
  • If the resource is hung up and the loading is abnormal, we can not always rely on the user to complain before we know it, we need to take the initiative to alarm.

With one-time refactoring, the increase in page load times on gigabit network speeds and 10,000 devices may be as low as 0.1%, but such numbers are not representative. Network environments and hardware devices vary greatly. For mid – and low-end devices, the subjective experience of performance improvement is more obvious, and the corresponding data changes are more representative.

Many projects upload resources to CDN. When some CDN nodes have problems, they generally cannot accurately inform “xx, your XX resource is down”, so we need to take the initiative to monitor.

According to Google data, when a page loads more than 10 seconds, users get desperate, often leave the current page, and most likely won’t come back.

With what?

For front-end Performance metrics, the W3C defines a powerful Performance API, It also includes High Resolution Time, Frame Timing, Navigation Timing, Performance Timeline, Resource Timing and User Timing And many other specific standards.

This article mainly covers Navigation Timing and Resource Timing. As of mid-2018, every major browser has completed the basic implementation.

One of the many functions of the Performance API is to record the Performance (time details) of the page itself and the resources on the page. All we have to do is query and use.

To view the API, you can type Performance directly into the browser console.

Next, we’ll use the window.Performance object provided by the browser (the concrete implementation of the Performance API) to implement a simple front-end performance monitoring tool.

Install a front-end performance monitoring tool in five minutes

First line of code

Name the tool pMonitor, which means Performance Monitor.

const pMonitor = {}
Copy the code

What indicators to Monitor

Since it is a “5 minutes to achieve an XXX” series, there are trade-offs. Therefore, this paper only selects the two most important indicators for monitoring:

  • Page load time
  • Resource request time

Looked at the time, has passed 4 minutes, xiaobian said the mood is stable, without a trace of fluctuations.

Page load

The performance metrics for page loading can be found in the Navigation Timing. Navigation Timing includes the time details of each link from the page requested to the page finished loading.

The Navigation Timing can be obtained in the following ways:

const navTimes = performance.getEntriesByType('navigation')
Copy the code

GetEntriesByType is one of the ways we get performance data. Performance also offers getEntries and getEntriesByName. Due to “time constraints”, the details are not detailed here, but you can go here: www.w3.org/TR/performa… .

The result is an array whose elements have the following structure:

{
  "connectEnd": 64.15495765894057."connectStart": 64.15495765894057."domainLookupEnd": 64.15495765894057."domainLookupStart": 64.15495765894057."domComplete": 2002.5385066728431."domContentLoadedEventEnd": 2001.7384263440083."domContentLoadedEventStart": 2001.2386167400286."domInteractive": 1988.638474368076."domLoading": 271.75174283737226."duration": 2002.9385468372606."entryType": "navigation"."fetchStart": 64.15495765894057."loadEventEnd": 2002.9385468372606."loadEventStart": 2002.7383663540235."name": "document"."navigationStart": 0."redirectCount": 0."redirectEnd": 0."redirectStart": 0."requestStart": 65.28225608537441."responseEnd": 1988.283025689508."responseStart": 271.75174283737226."startTime": 0."type": "navigate"."unloadEventEnd": 0."unloadEventStart": 0."workerStart": 0.9636893776343863
}
Copy the code

Navigation Timing Level 2 provides details about the time meaning of each field:

As you can see, it’s full of details. Therefore, there are a lot of things that can be calculated, such as DNS query time, TLS handshake time and so on. It can be said that only unexpected, nothing can be done

Since we’re focused on page loading, it’s natural to read domComplete:

const [{ domComplete }] = performance.getEntriesByType('navigation')
Copy the code

Define a method to get domComplete:

pMonitor.getLoadTime = (a)= > {
  const [{ domComplete }] = performance.getEntriesByType('navigation')
  return domComplete
}
Copy the code

At this point, we have an accurate page load time.

Resource to load

If a page has a Navigation Timing, does a static resource also have a Navigation Timing?

The answer is yes, it’s called Resource Timing. It contains the details of each link in the page from the time of sending the request to the time of loading. It is very similar to the Navigation Timing.

To obtain the load time of resources, use the keyword ‘resource’ as follows:

performance.getEntriesByType('resource')
Copy the code

As you can imagine, the returned result is usually a very long array because it contains the load information for all the resources on the page.

The specific structure of each message is:

{
  "connectEnd": 462.95008929525244."connectStart": 462.95008929525244."domainLookupEnd": 462.95008929525244."domainLookupStart": 462.95008929525244."duration": 0.9620853673520173."entryType": "resource"."fetchStart": 462.95008929525244."initiatorType": "img"."name": "https://cn.bing.com/sa/simg/SharedSpriteDesktopRewards_022118.png"."nextHopProtocol": ""."redirectEnd": 0."redirectStart": 0."requestStart": 463.91217466260445."responseEnd": 463.91217466260445."responseStart": 463.91217466260445."startTime": 462.95008929525244."workerStart": 0
}
Copy the code

Above for July 7, 2018, under the cn.bing.com searching for the test, the performance. The getEntriesByType (” resource “) returns the second result.

We focus on resource loading time, which can be obtained in the following form:

const [{ startTime, responseEnd }] = performance.getEntriesByType('resource')
const loadTime = responseEnd - startTime
Copy the code

Resource Timing Level 2 specifies the difference between startTime, fetchStart, connectStart and requestStart.

Not all resource load times need to be concerned, but the ones that load slowly are important.

For the sake of simplification, 10s is defined as the timeout limit. Then the method of obtaining timeout resources is as follows:

const SEC = 1000
const TIMEOUT = 10 * SEC
const setTime = (limit = TIMEOUT) = > time => time >= limit
const isTimeout = setTime()
const getLoadTime = ({ startTime, responseEnd }) = > responseEnd - startTime
const getName = ({ name }) = > name
const resourceTimes = performance.getEntriesByType('resource')
const getTimeoutRes = resourceTimes
  .filter(item= > isTimeout(getLoadTime(item)))
  .map(getName)
Copy the code

In this way, we get a list of all the resources that have timed out.

Simple encapsulation:

const SEC = 1000
const TIMEOUT = 10 * SEC
const setTime = (limit = TIMEOUT) = > time => time >= limit
const getLoadTime = ({ requestStart, responseEnd }) = >
  responseEnd - requestStart
const getName = ({ name }) = > name
pMonitor.getTimeoutRes = (limit = TIMEOUT) = > {
  const isTimeout = setTime(limit)
  const resourceTimes = performance.getEntriesByType('resource')
  return resourceTimes.filter(item= > isTimeout(getLoadTime(item))).map(getName)
}
Copy the code

Reported data

After obtaining the data, report it to the server:

// Generate the form data
const convert2FormData = (data = {}) = >
  Object.entries(data).reduce((last, [key, value]) = > {
    if (Array.isArray(value)) {
      return value.reduce((lastResult, item) = > {
        lastResult.append(`${key}[] `, item)
        return lastResult
      }, last)
    }
    last.append(key, value)
    return last
  }, new FormData())
// Concatenate the URL when GET
const makeItStr = (data = {}) = >
  Object.entries(data)
    .map(([k, v]) = > `${k}=${v}`)
    .join('&')
// Report data
pMonitor.log = (url, data = {}, type = 'POST') = > {
  const method = type.toLowerCase()
  const urlToUse = method === 'get' ? `${url}?${makeItStr(data)}` : url
  const body = method === 'get'? {}, {body: convert2FormData(data) }
  constoption = { method, ... body } fetch(urlToUse, option).catch(e= > console.log(e))
}
Copy the code

Let’s go back and initialize

Details such as the url for uploading data and the timeout period vary from project to project, so you need to provide an initialization method:

// Cache configuration
let config = {}
/** * @param {object} option * @param {string} option.url Address for reporting page loading data * @param {string} Option. timeoutUrl Address for reporting page resource timeout * @param {string=} [option.method='POST'] Request mode * @param {number=} [option.timeout=10000] */
pMonitor.init = option= > {
  const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option
  config = {
    url,
    timeoutUrl,
    method,
    timeout
  }
  // The binding event is used to trigger the reported data
  pMonitor.bindEvent()
}
Copy the code

When the trigger

Performance monitoring is a secondary function and should not block page loading, so we only get the data and report it when the page has finished loading (in fact, we don’t get the necessary information until the page has finished loading) :

// Encapsulate a method that reports two core data items
pMonitor.logPackage = (a)= > {
  const { url, timeoutUrl, method } = config
  const domComplete = pMonitor.getLoadTime()
  const timeoutRes = pMonitor.getTimeoutRes(config.timeout)
  // Report the page loading time
  pMonitor.log(url, { domeComplete }, method)
  if (timeoutRes.length) {
    pMonitor.log(
      timeoutUrl,
      {
        timeoutRes
      },
      method
    )
  }
}
// Event binding
pMonitor.bindEvent = (a)= > {
  const oldOnload = window.onload
  window.onload = e= > {
    if (oldOnload && typeof oldOnload === 'function') {
      oldOnload(e)
    }
    // Try not to affect the main page thread
    if (window.requestIdleCallback) {
      window.requestIdleCallback(pMonitor.logPackage)
    } else {
      setTimeout(pMonitor.logPackage)
    }
  }
}
Copy the code

summary

At this point, a complete front-end performance monitoring tool is complete.

const base = {
  log() {},
  logPackage() {},
  getLoadTime() {},
  getTimeoutRes() {},
  bindEvent() {},
  init() {}
}

const pm = (function() {
  // Forward compatibility
  if (!window.performance) return base
  constpMonitor = { ... base }let config = {}
  const SEC = 1000
  const TIMEOUT = 10 * SEC
  const setTime = (limit = TIMEOUT) = > time => time >= limit
  const getLoadTime = ({ startTime, responseEnd }) = > responseEnd - startTime
  const getName = ({ name }) = > name
  // Generate the form data
  const convert2FormData = (data = {}) = >
    Object.entries(data).reduce((last, [key, value]) = > {
      if (Array.isArray(value)) {
        return value.reduce((lastResult, item) = > {
          lastResult.append(`${key}[] `, item)
          return lastResult
        }, last)
      }
      last.append(key, value)
      return last
    }, new FormData())
  // Concatenate the URL when GET
  const makeItStr = (data = {}) = >
    Object.entries(data)
      .map(([k, v]) = > `${k}=${v}`)
      .join('&')
  pMonitor.getLoadTime = (a)= > {
    const [{ domComplete }] = performance.getEntriesByType('navigation')
    return domComplete
  }
  pMonitor.getTimeoutRes = (limit = TIMEOUT) = > {
    const isTimeout = setTime(limit)
    const resourceTimes = performance.getEntriesByType('resource')
    return resourceTimes
      .filter(item= > isTimeout(getLoadTime(item)))
      .map(getName)
  }
  // Report data
  pMonitor.log = (url, data = {}, type = 'POST') = > {
    const method = type.toLowerCase()
    const urlToUse = method === 'get' ? `${url}?${makeItStr(data)}` : url
    const body = method === 'get'? {}, {body: convert2FormData(data) }
    constinit = { method, ... body } fetch(urlToUse, init).catch(e= > console.log(e))
  }
  // Encapsulate a method that reports two core data items
  pMonitor.logPackage = (a)= > {
    const { url, timeoutUrl, method } = config
    const domComplete = pMonitor.getLoadTime()
    const timeoutRes = pMonitor.getTimeoutRes(config.timeout)
    // Report the page loading time
    pMonitor.log(url, { domeComplete }, method)
    if (timeoutRes.length) {
      pMonitor.log(
        timeoutUrl,
        {
          timeoutRes
        },
        method
      )
    }
  }
  // Event binding
  pMonitor.bindEvent = (a)= > {
    const oldOnload = window.onload
    window.onload = e= > {
      if (oldOnload && typeof oldOnload === 'function') {
        oldOnload(e)
      }
      // Try not to affect the main page thread
      if (window.requestIdleCallback) {
        window.requestIdleCallback(pMonitor.logPackage)
      } else {
        setTimeout(pMonitor.logPackage)
      }
    }
  }

  /** * @param {object} option * @param {string} option.url Address for reporting page loading data * @param {string} Option. timeoutUrl Address for reporting page resource timeout * @param {string=} [option.method='POST'] Request mode * @param {number=} [option.timeout=10000] */
  pMonitor.init = option= > {
    const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option
    config = {
      url,
      timeoutUrl,
      method,
      timeout
    }
    // The binding event is used to trigger the reported data
    pMonitor.bindEvent()
  }

  return pMonitor
})()

export default pm
Copy the code

How to? Is it not complicated? It’s even a little simple

Look at the time again, 5 minutes, whatever, but don’t worry about the details, Orz

added

call

The monitoring tool should not take up the JavaScript parsing time of the main thread while the page loads if you want to detect any defects. Therefore, it is better to load the page asynchronously after the onLoad event is triggered:

// At the bottom of the project entry file
const log = async() = > {const pMonitor = await import('/path/to/pMonitor.js')
  pMonitor.init({ url: 'xxx'.timeoutUrl: 'xxxx' })
  pMonitor.logPackage()
  // We can further remove the bindEvent method from the source code
}
const oldOnload = window.onload
window.onload = e= > {
  if (oldOnload && typeof oldOnload === 'string') {
    oldOnload(e)
  }
  // Try not to affect the main page thread
  if (window.requestIdleCallback) {
    window.requestIdleCallback(log)
  } else {
    setTimeout(log)
  }
}
Copy the code

Cross-domain and other request issues

The tool does not consider cross-domain issues when reporting data, nor does it deal with the presence of both GET and POST.

I don’t need a bike in five minutes!

If necessary, you can override the pmonitor. logPackage method and create

and
dynamically instead, or use the more common image tapping method ~

What about the cops? All the calls and no police? !

This still needs to cooperate with the server.

Each item can be assigned a different reporting URL or a uniform set of urls, and the item is assigned a unique ID to distinguish between them.

Notify the developer by email/SMS when the timeout number exceeds the agreed threshold within a specified period of time.

fine-grained

Now, only simple statistics are collected for timeout resources, but the specific timeout cause (DNS? TCP? The request? The response? This is left to the reader to optimize, give it a try ~

The next step

This article introduces performance monitoring in terms of page loading, parsing and execution of JavaScript code, and how fast the first screen of a page can be rendered (especially for single-page applications). In the next section, we’ll explore the Performance Timeline Level 2 and see how you can monitor JavaScript runtime Performance

The resources

  • The w3c. Making. IO/navigation -…
  • www.w3.org/TR/resource…
  • www.w3.org/TR/performa…
  • Developers.google.com/web/fundame…

About dance weekly

“Strange Dance Weekly” is a front end technology community operated by the professional front end team “Strange Dance Group” of 360 Company. Follow the public number, directly send links to the background can contribute to us.