completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

The performance timeline

performance.now, timeOrigin, PerformanceObserver, and buffered entries: the base every other API builds on. The clock's resolution, the buffers' limits and the entry types, recorded in Chrome, Firefox and WebKit.

By the end, you can
  • 01
    Read the browser's logbookSay what a performance entry holds, read entries back by type, and know which types only an observer gets.
  • 02
    Trust the clockKnow how finely performance.now() moves in each browser, and use timeOrigin to compare a page's times with a worker's.
  • 03
    Catch every entryObserve one type at a time with buffered: true, as early as you can, and notice a full logbook or a missing type.

One timeline for every API

Asha opens the Leaf and Kettle store on her phone, on the bus. The page loads. She taps Add to cart and puts her phone away. To her, it was one quick visit.

Her browser saw much more. While the page loaded, it noted when the visit started and when each file arrived. It noted when the first words were painted, and when the biggest picture was. It noted a box that jumped, and a script that kept the page busy. Every note has a time.

Here is a visit like hers, recorded in lesson 1.4: what her screen showed, and what her browser wrote down.

Asha's visit to the store. Left: her screen at 5,569 ms, a real frame Chrome captured: the store's header, a promotion, the mint tea picture, the heading Mint tea and the Add to cart button. Right: what her browser wrote down, in time order: at 0 ms, navigation: the visit starts; at 602 ms, resource: styles.css took 576 ms; at 603 ms, resource: app.js took 2,285 ms; at 603 ms, resource: hero.svg took 4,668 ms; at 2,894 ms, longtask: script busy for 145 ms; at 3,092 ms, paint: the first words; at 4,043 ms, layout-shift: a box jumps; at 5,271 ms, longtask: script busy for 204 ms; at 5,568 ms, largest-contentful-paint: the biggest picture.What Asha sawThe store: its header, a promotion, the mint tea picture, the heading Mint tea and the Add to cart button5,569 msWhat her browser wrote down0 msnavigationthe visit starts602 msresourcestyles.css took 576 ms603 msresourceapp.js took 2,285 ms603 msresourcehero.svg took 4,668 ms2,894 mslongtaskscript busy for 145 ms3,092 mspaintthe first words4,043 mslayout-shifta box jumps5,271 mslongtaskscript busy for 204 ms5,568 mslargest-contentful-paintthe biggest picture
Asha's visit: the store from lesson 1.4, loaded on Slow 4G with the CPU slowed 4 times, like her phone. Left: a real frame Chrome captured at 5,569 ms. Right: some of the notes her browser wrote, in time order. A file's note starts when the file is asked for, and says how long it took to arrive.

Each line is one note: when it started, what kind it was, and what it was about. Asha saw none of them.

You've read numbers like these in DevTools, in Lighthouse and in Chrome's field data. Now you want them from your own code, from every visitor. So where does the browser keep its notes? And how do you read them? This lesson answers both.

The notes live in the page's performance timeline: a list of timing entries the browser keeps for each page. In Custom tracks and annotations, you wrote your own marks in it, like lines in the shop's logbook. That logbook isn't only yours. The browser's performance APIs write in it too.

Real-life analogyEveryone at the shop writes in one logbook

Leaf and Kettle keeps one logbook on the counter, and every member of staff writes in it. Ravi writes when the milk is delivered. The barista writes when the kettle boils. Whoever mops up a spill writes that down too. Every line has the same four parts: what kind of thing happened, what it was about, when it started, and how long it took.

In real life: The logbook on the counter
In JavaScript: The page's performance timeline
In real life: Each member of staff
In JavaScript: Each performance API: Navigation Timing, Resource Timing, Paint Timing and more
In real life: The kind of thing: "delivery" or "spill"
In JavaScript: entryType, like resource or layout-shift
In real life: What it was about: "milk"
In JavaScript: name, like a file's address
In real life: When it started, and how long it took
In JavaScript: startTime and duration, in milliseconds

Here are those four parts on a real entry. Write one yourself, the way Custom tracks did, and read it back:

Write one entry, then read its four partsPop out in the code editor (opens in a new tab)JavaScript
// Write one line in the logbook yourself, as in Custom tracks.performance.mark("Add to cart"); // Ask the logbook for it by name, and read its four parts.const entry = performance.getEntriesByName("Add to cart")[0];console.log(`Its entryType is ${entry.entryType}.`);console.log(`Its name is ${entry.name}.`);console.log(`Its startTime is a ${typeof entry.startTime}, in milliseconds.`);console.log(`Its duration is ${entry.duration} ms.`);
  • Line 2 writes a mark named Add to cart. As in Custom tracks, a mark is one moment.
  • Line 5 asks the timeline for the entries with that name, and takes the first. getEntriesByName gives back a list, because two entries can share a name.
  • Lines 6 and 7 print Its entryType is mark. and Its name is Add to cart.
  • Line 8 prints Its startTime is a number, in milliseconds. The number says when the mark was written, counted from the start of the page. It's different every time you run the code, so the example only checks what kind of value it is.
  • Line 9 prints Its duration is 0 ms. A moment has no length.

Every entry from every API has these four parts. A file the page fetched gets an entry whose type is resource. Its name is the file's address, and its duration is how long the file took. Each API also adds fields of its own, which the next lessons read.

The course loaded the store in Chrome and looked for six kinds of entries, two ways. The picture shows what each way found:

The store's page in Chrome. getEntriesByType found: 1 navigation, 3 resource, 2 paint. It found none of the other three, but a buffered observer got: 1 largest-contentful-paint, 1 layout-shift, 1 longtask.getEntriesByType foundnavigation: 1resource: 3paint: 2Only an observer gotlargest-contentful-paint: 1layout-shift: 1longtask: 1
The store's page in Chrome. getEntriesByType found the entries on the left. It skips the types on the right: only an observer got those.

Look at the right side. The page had a largest paint, a layout shift and a long task, and Chrome measured all three. Yet when the course asked getEntriesByType for them, it got nothing back. Only an observer got them: a piece of code that asked to hear about new entries. So the two ways of reading the timeline don't see the same things.

The main idea

The browser writes what it measures as entries in one performance timeline per page. Each entry has a type, a name, a start time and a duration, all timed by one clock. You can read some types back with getEntriesByType, or ask an observer to tell you. Only an observer, started early with buffered: true, catches every type.

The rest of the lesson takes this one piece at a time:

  • The clock: where every time comes from, and how fine it is.
  • Entry types: which kinds you can read back, and which only an observer gets.
  • Observers: how to be told, even about entries written before you asked.
  • Support: which browsers write which types.

Measuring performance showed the basics of the stopwatch and of observers. This lesson adds the rules that decide what you can read, and what you miss.

High resolution time and timeOrigin

One clock for every entry

Every entry has a startTime. All of them come from the same clock, the one you read with performance.now(). It gives the time in milliseconds since the page started.

The moment the page started is its time origin. For a page, the High Resolution Time standard sets it at the moment the navigation started. That's the 0 that every time in The navigation timeline counted from.

Real-life analogyMinutes since the shop opened

Leaf and Kettle's logbook doesn't use the time of day. Each line says how many minutes after opening it happened: "delivery, minute 70". A second branch across town opens an hour later, and its logbook starts at minute 0 too. So the branch's minute 10 is the main shop's minute 70. To compare the two books, you write each line as opening time plus minutes.

In real life: The moment the shop opened
In JavaScript: The time origin, performance.timeOrigin
In real life: Minutes since opening, on each line
In JavaScript: performance.now() and every startTime
In real life: The branch that opened an hour later
In JavaScript: A worker, with its own time origin
In real life: Opening time plus minutes
In JavaScript: performance.timeOrigin + performance.now()

Why not use the computer's own clock, Date.now()? Because that clock can be set. The computer moves it forward or back to match the real time. The standard's own example warns that a duration measured with Date.now() can come out positive, negative or zero.

performance.now() reads a clock that only ever moves forward, like a stopwatch. Nobody sets a stopwatch back in the middle of a race, so a duration measured with it is never negative.

The time origin itself is in performance.timeOrigin, in milliseconds since 1970. Add the two, and you get the time on the same scale as Date.now(). In every engine the course tried, performance.timeOrigin + performance.now() stayed within 2 ms of Date.now().

How fine is the clock?

A clock doesn't move smoothly. It ticks. The smallest step between two readings is its resolution. A kitchen clock with only a minute hand can't time a 20-second job: it says 0 minutes or 1.

performance.now() could tick in steps far smaller than a millisecond, but browsers make the steps bigger on purpose. A very fine clock can help a harmful site time tiny things. From those times, it can guess data it isn't allowed to see, or tell one visitor from another. MDN says the clock is coarsened "to offer protection against timing attacks and fingerprinting". The standard sets the finest step a page may get:

  • On a plain page: 100 microseconds (µs), or coarser if the browser picks. A microsecond is a thousandth of a millisecond.
  • On a cross-origin isolated page: 5 µs, or coarser.

A page becomes cross-origin isolated when the server sends it with these two headers:

The two headers that isolate a pageHTTP
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Together, they keep the page apart from other sites. It shares its group of windows only with pages from its own origin: the same scheme, host and port. And it only loads other sites' files when they agree to it. In return, the browser trusts it with a finer clock.

The course measured the smallest step in each engine. It called performance.now() 200,000 times in a row and kept the smallest change. First, on a plain page:

The clock's smallest step on a plain pagePop out in the code editor (opens in a new tab)JavaScript
// The smallest step performance.now() took in 200,000 calls, on a plain page.// In microseconds (µs): a thousandth of a millisecond.const plainStep = { Chrome: 100, Firefox: 1000, WebKit: 1000 }; for (const [browser, step] of Object.entries(plainStep)) {  console.log(`${browser}'s clock moves in steps of ${step / 1000} ms.`);}
  • Lines 1 and 2 say what the numbers are: the smallest step, in microseconds.
  • Line 3 holds the recorded steps.
  • Line 6 divides by 1,000 to turn microseconds into milliseconds. It prints Chrome's clock moves in steps of 0.1 ms., Firefox's clock moves in steps of 1 ms. and WebKit's clock moves in steps of 1 ms.

Chrome used the finest step the standard allows. Firefox and WebKit picked steps ten times coarser, which the standard also allows. Why it matters: in Firefox on a plain page, a job that takes 0.4 ms reads as 0 ms or 1 ms. One reading tells you little. Time the job many times and average, before you trust a difference smaller than a millisecond.

Then the same page, sent with the two headers:

The clock's smallest step on an isolated pagePop out in the code editor (opens in a new tab)JavaScript
// The same 200,000 calls, on a page sent with the two isolation headers.const isolatedStep = { Chrome: 5, Firefox: 20, WebKit: 20 }; for (const [browser, step] of Object.entries(isolatedStep)) {  console.log(`${browser}'s clock moves in steps of ${step / 1000} ms.`);}
  • Line 2 holds the recorded steps on the isolated page.
  • Line 5 prints Chrome's clock moves in steps of 0.005 ms., Firefox's clock moves in steps of 0.02 ms. and WebKit's clock moves in steps of 0.02 ms.

Isolation made every clock finer: 20 times finer in Chrome, 50 times in Firefox and WebKit. Date.now() didn't change. It moved in steps of 1 ms everywhere, on both pages. Now measure the browser you're reading this in:

Playground: your browser's clock
Smallest step of performance.now()plain page, isolated page
Chrome 156
100 µs, 5 µs
Firefox 155
1,000 µs, 20 µs
WebKit 26
1,000 µs, 20 µs
This browser
not measured yet
Try it yourself
performance.now(), in this tab

The button calls performance.now() 200,000 times in a row, then does the same with Date.now(). It keeps the smallest step each clock took.

Live: the measurement runs in this tab. The rows above yours are the course's recordings: a plain page first, then a cross-origin isolated one.

The button runs the same loop as the course's recording, here in this tab. It also tells you whether this page is cross-origin isolated, so you know which number in each row to compare with.

Pages and workers have their own clocks

A worker is a script that a page starts to run beside it, so heavy work doesn't freeze the page. Each worker gets its own time origin: the moment it started running. Its performance.now() counts from there, like the branch that opened later.

The course started a worker in each engine, half a second after its page had loaded, and compared the two time origins. In Chrome:

One moment, two clocksPop out in the code editor (opens in a new tab)JavaScript
// In Chrome, the course started a worker after its page had loaded.// The worker's time origin came 531.6 ms after the page's.const workerStartedAt = 531.6; const workerClock = 0; // the worker's performance.now(), the moment it startedconsole.log(`The worker's clock says ${workerClock} ms.`);console.log(`At that moment, the page's clock says ${workerClock + workerStartedAt} ms.`);
  • Lines 1 and 2 say what was recorded: the worker's time origin came 531.6 ms after the page's.
  • Line 5 takes the worker's clock at the moment the worker started, when it reads 0.
  • Line 6 prints The worker's clock says 0 ms. Line 7 prints At that moment, the page's clock says 531.6 ms.
Two clocks in Chrome: the page's performance.now() counts from the page's time origin; a worker started later counts from its own, which was 531.6 ms after the page's. Worker time plus 531.6 ms is page time.Pagepage's performance.now()Workerworker's now()0page 531.6 ms = worker 0
Each clock counts from its own start. The worker's 0 is the page's 531.6 ms, so add each one's timeOrigin before you compare them.

Across the three engines, the gap was between 513 and 677 ms. Why it matters: say a worker reports that a job ended at 20 ms. That's 20 ms on the worker's clock, not the page's. Put it on the page's timeline as it is, and the job lands half a second too early.

The fix is the standard's own example. Send the time on a scale both clocks share, then turn it back:

Put a worker's time on the page's clockJavaScript
// In the worker: send the time as "ms since 1970", which every clock shares.postMessage({ doneAt: performance.timeOrigin + performance.now() }); // In the page: turn it back into the page's own clock.worker.onmessage = (event) => {  const doneAt = event.data.doneAt - performance.timeOrigin;  console.log(`The worker finished at ${doneAt.toFixed(1)} ms, page time.`);};
  • Line 2 runs in the worker. It adds the worker's own time origin, which turns the time into milliseconds since 1970.
  • Line 6 runs in the page. It takes away the page's time origin, which turns the time back into the page's clock.
  • Line 7 prints the result, on the page's clock.

That's the "opening time plus minutes" trick from the branch shop. It needs performance.timeOrigin, which Chrome, Firefox and Safari all support:

Entry types

Back to the logbook. Every line has a kind: a delivery, a spill, a boiled kettle. In the timeline, the kind is the entry's entryType. Each API writes its own types. Here are the ones this course uses:

  • navigation: the page itself, from Navigation Timing. The navigation timeline read it job by job.
  • resource: one for each file the page fetched, from Resource Timing.
  • paint: the first paints, from Paint Timing.
  • largest-contentful-paint, layout-shift and longtask: the entries behind LCP, CLS and long tasks.
  • event: slow interactions, the entries behind INP.
  • mark and measure: your own, from User Timing.

The W3C keeps the full list in one place, the Timing Entry Names Registry. For each type, the registry answers two questions:

  • Can getEntriesByType read it back? That's availableFromTimeline.
  • How many entries of this type does the browser keep? That's maxBufferSize. A buffer is the space where the browser keeps entries for later: the type's pages in the logbook.

Here is what the registry says for six of them:

From the registry
  • event: observers only, keeps up to 150
  • largest-contentful-paint: observers only, keeps up to 150
  • layout-shift: observers only, keeps up to 150
  • longtask: observers only, keeps up to 200
  • paint: getEntriesByType reads it, keeps up to 2
  • resource: getEntriesByType reads it, keeps up to 250

Four of these six are "observers only". Three of them are the right side of the store's picture. Think of their lines as staff-only pages at the back of the logbook. The browser still keeps them, but you can't leaf through them yourself. Only a colleague who asked to be told will read them out to you.

Reading the logbook

To read what the logbook kept, ask for one type with performance.getEntriesByType(). It's like asking "show me every delivery so far". You get a list of the entries of that type, in the order they started. Here is what it gave back for files when the course loaded the store in Chrome:

What the logbook gave backPop out in the code editor (opens in a new tab)JavaScript
// The store's page in Chrome, after it loaded:// performance.getEntriesByType("resource") gave back three entries.const files = ["styles.css", "hero.svg", "app.js"]; console.log(`The logbook lists ${files.length} files: ${files.join(", ")}.`);
  • Lines 1 and 2 say where the list came from.
  • Line 3 holds the file names of the three entries Chrome gave back.
  • Line 5 prints The logbook lists 3 files: styles.css, hero.svg, app.js.

Those are the store's three files. Firefox's logbook had a fourth, favicon.ico, the tab's little icon. The logbook holds what each browser really fetched, so it can differ from one browser to the next.

The types getEntriesByType skips

Now ask for the three types on the right of the picture. The course asked getEntriesByType, then a buffered observer, on the same page at the same moment:

Measured and kept, but skippedPop out in the code editor (opens in a new tab)JavaScript
// The same page in Chrome. For each type: entries from getEntriesByType,// then entries a buffered observer was told about.const counts = {  "largest-contentful-paint": [0, 1],  "layout-shift": [0, 1],  longtask: [0, 1],}; for (const [type, [found, told]] of Object.entries(counts)) {  console.log(`${type}: ${found} from getEntriesByType, ${told} from an observer`);}
  • Lines 1 and 2 say what the two numbers are.
  • Lines 4 to 6 hold two recorded counts for each type: entries from getEntriesByType, then entries told to the observer.
  • Line 10 prints one line per type. The first is largest-contentful-paint: 0 from getEntriesByType, 1 from an observer.
  • Then come layout-shift: 0 from getEntriesByType, 1 from an observer and longtask: 0 from getEntriesByType, 1 from an observer.

Chrome measured and kept all three: an observer that started after the load still got them. But getEntriesByType doesn't read those types at all. Firefox and WebKit did the same with LCP: none from getEntriesByType, but their observers were told.

Why it matters: code that reads LCP with getEntriesByType gets an empty list, every time, in every browser. The Performance Timeline standard warns about this: "new performance API's and metrics may only be available through the PerformanceObserver interface." So the next section is about observers.

PerformanceObserver and buffered entries

Real-life analogyA look in the logbook, or a tap on the shoulder

You need to know when the milk arrives. You could walk to the counter every few minutes and read the logbook. Or you could ask Ravi: "Tap me on the shoulder each time a delivery is written in." Then you get on with your work, and he tells you, a moment after each one.

In real life: Walking over to read the logbook
In JavaScript: performance.getEntriesByType(): what's written so far
In real life: "Tap me on the shoulder for each delivery"
In JavaScript: observer.observe({ type: "resource" })
In real life: Ravi, who remembers to tap you
In JavaScript: A PerformanceObserver
In real life: The tap, a moment after the line is written
In JavaScript: Your function, called with the new entries

A PerformanceObserver is Ravi. You make one with a function, then tell it which type to watch:

Ask to be told about each new markPop out in the code editor (opens in a new tab)JavaScript
const observer = new PerformanceObserver((list) => {  for (const entry of list.getEntries()) {    console.log(`New ${entry.entryType}: ${entry.name}`);  }});observer.observe({ type: "mark" }); performance.mark("Add to cart");console.log("The mark is written.");
  • Lines 1 to 5 make the observer. Its function gets a list of new entries, and line 3 prints each one.
  • Line 6 says what to watch: one type, mark.
  • Line 8 writes a mark. Line 9 prints The mark is written.
  • Only then does the observer's function run. It prints New mark: Add to cart.

Notice the order. The browser doesn't call your function in the middle of mark(). It collects the new entries and calls you a moment later, in a task of its own. The standard puts that task in a low-priority queue, run when the browser is idle if it can be. So watching costs the page little.

The standard also lists what you gain over reading the logbook. You don't have to keep coming back to check. And you never get the same entry twice, so you don't have to sort out which ones are new.

Also tell me about the ones written before

An observer only hears about entries written after it starts. That's a problem. The most important entries come early, while the page loads, and your analytics script might load seconds later.

So you ask Ravi one more thing: "And tell me about the ones you wrote before I asked." That's the buffered flag. In the standard's words, browsers "buffer some number of events while the page is being constructed". The flag is how an observer gets them.

Also told about an earlier markPop out in the code editor (opens in a new tab)JavaScript
// The mark is written first, before anyone is listening.performance.mark("Page ready"); const observer = new PerformanceObserver((list) => {  for (const entry of list.getEntries()) console.log(`Told about ${entry.name}.`);});observer.observe({ type: "mark", buffered: true });
  • Line 2 writes a mark before any observer exists.
  • Lines 4 to 6 make an observer that prints each entry it's told about.
  • Line 7 watches marks, with buffered: true. The observer's first call brings the mark from line 2. It prints Told about Page ready.

Take buffered: true out of line 7, and the code prints nothing. The mark was written before anyone asked.

Two rules come with the flag:

  • It works with type, one type per call. To watch three types, call observe() three times. The older form, observe({ entryTypes: [...] }), takes no other options. Chrome ignores the flag there and prints a warning.
  • It only brings back what the logbook still holds. That depends on how much room the logbook has.

The logbook has a set number of pages

Real-life analogyA logbook with room for 250 deliveries

Leaf and Kettle's logbook has room for 250 deliveries. On a busy day, delivery 251 arrives and the book is full, so nobody writes it down. Ravi still taps everyone who asked to be told, so they hear about every delivery. But anyone who opens the book later finds only the first 250.

In real life: Room for 250 deliveries
In JavaScript: The resource buffer, 250 entries in every browser the course tried
In real life: Deliveries after the book is full
In JavaScript: Resource entries the timeline doesn't keep
In real life: A colleague who asked to be told, early
In JavaScript: An observer that started before the files loaded
In real life: Opening the book later
In JavaScript: getEntriesByType(), or a buffered observer that starts late

Every type has its own size, from the registry. For files, the Resource Timing standard says: "The recommended minimum number of PerformanceResourceTiming objects is 250, though this may be changed by the user agent." A page with many images, or many calls to an API, can pass that.

The course built a page with 300 images, each its own file. An observer started first thing in the page, before any image. After the load, the course read the logbook, and started a second buffered observer:

300 images, read three waysPop out in the code editor (opens in a new tab)JavaScript
// A page with 300 images, each its own file, in Chrome.const seen = { earlyObserver: 300, logbook: 250, lateObserver: 250 }; console.log(`An observer that started first was told about ${seen.earlyObserver}.`);console.log(`getEntriesByType found ${seen.logbook}.`);console.log(`A buffered observer that started after the load got ${seen.lateObserver}.`);
  • Line 2 holds Chrome's three recorded counts.
  • Line 4 prints An observer that started first was told about 300.
  • Line 5 prints getEntriesByType found 250.
  • Line 6 prints A buffered observer that started after the load got 250.

Firefox and WebKit gave the same three numbers. The early observer heard about every file, because the browser tells observers before it checks for room. The logbook stopped at 250. The late observer could only get what the logbook held.

300 resource entries in Chrome: the logbook kept 250 and the buffer-full event fired; droppedEntriesCount reported 50; an observer registered from the start received all 300.The logbook250 kept50not keptObserver from the start300 received
The logbook kept the first 250 files and had no room for the last 50. The observer that was already listening was told about every one as it came.

Why it matters: a script that reads files late reports 250 on a page that loaded 300, and nothing says so. Chrome, at least, can tell you. The first time it calls a buffered observer, it passes a third argument with droppedEntriesCount: how many entries of the watched type the logbook didn't keep.

Ask how many the logbook droppedJavaScript
// Started after the 300 images had loaded.new PerformanceObserver((list, observer, options) => {  console.log(`Told about ${list.getEntries().length} files.`);  console.log(`The logbook dropped ${options.droppedEntriesCount}.`);}).observe({ type: "resource", buffered: true });
  • Line 2 gives the observer's function three parameters. The third, options, holds the count.
  • Line 3 prints how many files the observer was told about. In Chrome, after the 300 images, that was 250.
  • Line 4 prints the count. Chrome said 50.

Firefox and WebKit didn't report the count, so check that it's there before you use it:

Make room before the files load

The fix is a bigger logbook, asked for before the files start loading. One call asks for the room, and one event tells you if the logbook fills up anyway:

Make room for 400 filesJavaScript
// Make room for 400 resource entries, before the files start loading.performance.setResourceTimingBufferSize(400); // Hear about it if the logbook fills up anyway.performance.addEventListener("resourcetimingbufferfull", () => {  console.log("The resource logbook is full.");});
  • Line 2 asks for room for 400 resource entries. The standard calls it a request to change the limit.
  • Lines 5 to 7 listen for resourcetimingbufferfull. The browser fires it when the resource logbook is full and more entries are waiting.

With room for 400, the course's page kept all 300 files in Chrome. Try each engine, with both sizes:

Playground: 300 images, three ways
Resource entriesof 300
Observer that started first300
getEntriesByType250
Observer after the load250
Try it yourself
Chrome, buffer as it is

The logbook kept 250 of the 300 files. The buffer-full event fired once. droppedEntriesCount: 50.

Recorded on Apple M1 Pro. The page has 300 images, each its own file. One buffered observer started before them; a second one started after the load.

Each bar is one way of reading the files. The note under the buttons says how often the buffer-full event fired, and what droppedEntriesCount said.

Here are the two ways to read the timeline side by side. Read each row across: a question, then each way's answer.

Reading the logbook, or being told
The questiongetEntriesByType()A buffered observer
When does it answer?Once, when you call it.Each time new entries are written, a moment later.
Which entries does it give?Every entry of that type the logbook kept so far.With buffered: true, the kept ones first. Then each new one.
Does it see LCP, layout shifts and long tasks?No. It skips those types.Yes.
Does it see entries a full logbook didn't keep?No. They're gone.Yes, if it started before they were written.

Support across browsers

Not every browser writes every type. Before you ask Ravi to watch for spills, check that the shop writes spills down at all. In code, that's PerformanceObserver.supportedEntryTypes: the list of types this browser writes.

Chrome listed 15 types. Firefox and WebKit listed the same 8. Neither had layout-shift or longtask, so neither reports layout shifts or long tasks this way.

The standard's own example checks the list first, "to know when the entry types we would like to use are not supported." Here's that check, with the list Firefox gave:

Check before you watchPop out in the code editor (opens in a new tab)JavaScript
// PerformanceObserver.supportedEntryTypes, as Firefox listed it.const supported = ["event", "first-input", "largest-contentful-paint", "mark", "measure", "navigation", "paint", "resource"]; for (const type of ["largest-contentful-paint", "layout-shift", "longtask"]) {  console.log(`${type}: ${supported.includes(type) ? "observe it" : "skip it"}`);}
  • Line 2 is Firefox's recorded list.
  • Lines 4 to 6 check three types. They print largest-contentful-paint: observe it, layout-shift: skip it and longtask: skip it.

What if you skip the check? Watching a type the browser doesn't write does nothing. The standard says to stop there, and suggests a warning in the console, which Chrome prints. No error is thrown, and your function is never called.

Why it matters: say your analytics starts CLS at 0 and adds up the layout shifts. A Firefox visitor then reports a CLS of 0. That looks perfect, but it only means Firefox didn't measure it. Report a missing type as missing, not as 0.

Timeline or observer?

Sort each entry type by how you can read it. Ask the registry's question: does getEntriesByType give it back, or does only an observer get it?

Timeline or observer?
  • navigation
  • resource
  • paint
  • mark
  • largest-contentful-paint
  • layout-shift
  • longtask
  • event
Try it yourself
0 of 8 correct

Sort each entry type by where you can read it.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "performance.now() is exact to the microsecond." No. Browsers round it on purpose. On a plain page, Firefox and WebKit moved in steps of 1 ms.
  • "A worker's performance.now() matches the page's." No. Each counts from its own time origin. In Chrome, the worker's 0 was the page's 531.6 ms.
  • "getEntriesByType returns everything the browser measured." No. It skips LCP, layout shifts and long tasks. Only an observer gets them.
  • "The logbook keeps every entry." No. Each type has a size. The resource logbook kept 250 of 300 files in every engine.
  • "An observer hears about entries from before it started." No. Only with buffered: true, and only the ones the logbook still holds.
  • "A CLS of 0 from Firefox means nothing moved." No. Firefox doesn't write layout shifts at all. Report it as missing.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upA coarser clock in WebKit

Suppose WebKit's step on an isolated page were 100 µs instead of 20. What does the third line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// The same 200,000 calls, on a page sent with the two isolation headers.
const isolatedStep = { Chrome: 5, Firefox: 20, WebKit: 100 };

for (const [browser, step] of Object.entries(isolatedStep)) {
  console.log(`${browser}'s clock moves in steps of ${step / 1000} ms.`);
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upAn engine with layout shifts

    This engine also lists layout-shift. What does the second line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // PerformanceObserver.supportedEntryTypes, if an engine added layout shifts.
    const supported = ["event", "first-input", "largest-contentful-paint", "layout-shift", "mark", "measure", "navigation", "paint", "resource"];
    
    for (const type of ["largest-contentful-paint", "layout-shift", "longtask"]) {
      console.log(`${type}: ${supported.includes(type) ? "observe it" : "skip it"}`);
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFirefox's full logbook

      In Firefox, with the buffer as it is, how many times did the buffer-full event fire?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRoom for 400 in WebKit

        In WebKit, with the buffer set to 400, how many files was the observer after the load told about?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeAnother way to make room

          Resource Timing says a resourcetimingbufferfull handler should make room in one of two ways. One is setResourceTimingBufferSize. Which method is the other?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeThe first input's buffer

            In the Timing Entry Names Registry, how many entries does the first-input type's buffer keep?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask three things: which clock, which logbook, and whether the type goes in the logbook at all.

              Performance timeline quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the second line of the plain-page clock example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The smallest step performance.now() took in 200,000 calls, on a plain page.
                // In microseconds (µs): a thousandth of a millisecond.
                const plainStep = { Chrome: 100, Firefox: 1000, WebKit: 1000 };
                
                for (const [browser, step] of Object.entries(plainStep)) {
                  console.log(`${browser}'s clock moves in steps of ${step / 1000} ms.`);
                }

                Choose an answer to see the explanation.

              2. Question 2 of 7What does the observer example print first?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const observer = new PerformanceObserver((list) => {
                  for (const entry of list.getEntries()) {
                    console.log(`New ${entry.entryType}: ${entry.name}`);
                  }
                });
                observer.observe({ type: "mark" });
                
                performance.mark("Add to cart");
                console.log("The mark is written.");

                Choose an answer to see the explanation.

              3. Question 3 of 7On a plain page, Firefox's performance.now() moved in steps of a whole millisecond. Is that allowed?

                Choose an answer to see the explanation.

              4. Question 4 of 7A buffered observer that started after the load got 250 of a page's 300 resource entries. Why?

                Choose an answer to see the explanation.

              5. Question 5 of 7How do you put a worker's performance.now() on the same clock as the page's?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why does performance.getEntriesByType("largest-contentful-paint") give back an empty list?

                Choose an answer to see the explanation.

              7. Question 7 of 7An analytics script loads two seconds after the page starts. Which call still gets the page's earlier layout shifts?

                Choose an answer to see the explanation.

              Key takeaways

              • The browser writes what it measures as entries in each page's performance timeline.
              • Every entry has an entryType, a name, a startTime and a duration.
              • Every time comes from performance.now(): milliseconds since the time origin, rounded on purpose.
              • Each page and each worker has its own time origin. Add performance.timeOrigin before you compare them.
              • getEntriesByType reads back what the logbook kept, but it skips LCP, layout shifts and long tasks.
              • Observe one type at a time with buffered: true, as early as you can. The resource logbook kept only 250.
              • Check supportedEntryTypes, and report a missing type as missing.

              Remember the one-liner.
              The browser keeps a logbook; ask to be told, early and buffered, about the types it supports.

              Sources

              The facts on this page come from these standards, docs and browser code:

              Coming next: Navigation Timing and Resource Timing. You'll open the navigation and resource entries field by field, and see what other sites' files hide.

              CompleteFrontend Clear concepts. Working examples.