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.
- 01Read the browser's logbookSay what a performance entry holds, read entries back by type, and know which types only an observer gets.
- 02Trust the clockKnow how finely performance.now() moves in each browser, and use timeOrigin to compare a page's times with a worker's.
- 03Catch 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.
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.
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, likeresourceorlayout-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:
startTimeandduration, 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 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.
getEntriesByNamegives back a list, because two entries can share a name. - Lines 6 and 7 print
Its entryType is mark.andIts 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:
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 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.
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 everystartTime - 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:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpTogether, 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 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.andWebKit'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 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.andWebKit'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:
- 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
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.
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:
// 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 printsAt that moment, the page's clock says 531.6 ms.
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:
// 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-shiftandlongtask: the entries behind LCP, CLS and long tasks.event: slow interactions, the entries behind INP.markandmeasure: 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
getEntriesByTyperead it back? That'savailableFromTimeline. - 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:
event: observers only, keeps up to 150largest-contentful-paint: observers only, keeps up to 150layout-shift: observers only, keeps up to 150longtask: observers only, keeps up to 200paint: getEntriesByType reads it, keeps up to 2resource: 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:
// 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:
// 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 observerandlongtask: 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
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:
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.
// 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 printsTold 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, callobserve()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
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:
// 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.
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.
// 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 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:
The logbook kept 250 of the 300 files. The buffer-full event fired once. droppedEntriesCount: 50.
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.
| The question | getEntriesByType() | 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:
// 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 itandlongtask: 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?
- navigation
- resource
- paint
- mark
- largest-contentful-paint
- layout-shift
- longtask
- event
Sort each entry type by where you can read it.
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.
Suppose WebKit's step on an isolated page were 100 µs instead of 20. What does the third line print?
// 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.`);
}WebKit's clock moves in steps of 0.1 ms. 100 µs is a tenth of a millisecond. That's still allowed: the standard sets the finest step a browser may use, and a browser can pick a coarser one.
This engine also lists layout-shift. What does the second line print?
// 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"}`);
}layout-shift: observe it. The check only reads the list, so it follows whatever this browser writes.
In Firefox, with the buffer as it is, how many times did the buffer-full event fire?
2. Chrome and WebKit fired it once. Either way, the event means the same thing: the resource logbook is full, and more entries are waiting.
In WebKit, with the buffer set to 400, how many files was the observer after the load told about?
300: all of them. With room for 400, the logbook kept every file, so the late observer got every file too.
Resource Timing says a resourcetimingbufferfull handler should make room in one of two ways. One is setResourceTimingBufferSize. Which method is the other?
performance.clearResourceTimings(). It empties the resource logbook, so new files fit again. Read or send the entries you need first, or they're gone.
In the Timing Entry Names Registry, how many entries does the first-input type's buffer keep?
1. The browser keeps only the page's first input. Unlike event, this type is in the logbook.
Check your understanding
For each question, ask three things: which clock, which logbook, and whether the type goes in the logbook at all.
Question 1 of 7What does the second line of the plain-page clock example print?
Read the code, then predict// 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.
Question 2 of 7What does the observer example print first?
Read the code, then predictconst 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.
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.
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.
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.
Question 6 of 7Why does performance.getEntriesByType("largest-contentful-paint") give back an empty list?
Choose an answer to see the explanation.
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:
- Performance Timeline, W3C.
- High Resolution Time, W3C.
- Performance: now() method, security requirements, MDN.
- Window: crossOriginIsolated property, MDN.
- Timing Entry Names Registry, W3C.
- Resource Timing: the resource timing buffer, W3C.
- PerformanceObserver::observe, Blink's source code, Chromium.
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.