Skip to content

Commit

Permalink
update examples and polyfill
Browse files Browse the repository at this point in the history
  • Loading branch information
jwilliams720 committed Aug 22, 2024
1 parent 3930758 commit aa3d727
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 55 deletions.
1 change: 1 addition & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
node-version: 21
- name: Install & build TypeScript
run: |
cd polyfill
npm ci
npm run build
- name: Setup Pages
Expand Down
Binary file added docs/img/container-timing-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 17 additions & 2 deletions docs/polyfill.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
## Container Timing: Polyfill

This polyfill should be loaded in the head or as early as possible so it can annotate elements needed for timing when the observer runs. At the very latest it should be loaded before you make the call to initiate the observer.
This polyfill simulates Container Timing in your browser, it relies heavily on element-timing internally which means as of this writing it only works on Chromium-based browsers.

Once added to the top of your page you can then use the `ContainerPerformanceObserver` to mark entries. The `ContainerPerformanceObserver` behaves very similarly to the `PerformanceObserver` but is only useful for this specific metric. You will also need to mark containers you're interested in tracking with the `containertiming` attribute (See [update below](#update-22022024)), just like you would on individual elements. See the example below:
This will need to be loaded in the head or as early as possible so that it can override the built-in PerformanceObserver and mark elements needing to be timed (those underneath a `containertiming` attribute).

Once added, you can mark containers you're interested in with the `containertiming` attribute and use the `container` entryType in the PerformanceObserver. You can also see the examples folder for an idea on how to use the polyfill.

## Setup

Right now this polyfill is not on npm, so you will need to build and run locally. Go to the polyfill directory then run these steps:

- `npm i`
- `npm run build`
- Open up one of the example html files
- Check the dev tools console

## Demo

![img](../docs/img/container-timing-demo.gif)

**Markup**

Expand Down
14 changes: 7 additions & 7 deletions examples/adding-content/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
window.ctDebug = true;
const observer = new ContainerPerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(entry);
});
});
// const observer = new ContainerPerformanceObserver((list) => {
// list.getEntries().forEach((entry) => {
// console.log(entry);
// });
// });

const nativeObserver = new PerformanceObserver((v) => {
console.log(v);
console.log(v.getEntries());
});

nativeObserver.observe({ entryTypes: ["container"] });
observer.observe({ nestedStrategy: "transparent" });
// observer.observe({ nestedStrategy: "transparent" });

window.setTimeout(() => {
const innerContainer = document.querySelector(".container div");
Expand Down
2 changes: 1 addition & 1 deletion examples/shadow-dom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const observer = new PerformanceObserver((list) => {
});
});

observer.observe({ type: "element", buffered: true });
observer.observe({ entryTypes: ["container"] });

window.setTimeout(() => {
const host = document.querySelector("#host");
Expand Down
2 changes: 1 addition & 1 deletion examples/svg/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<title>Document</title>
</head>
<body>
<div style="width: 25em">
<div style="width: 25em" containertiming>
<svg
viewBox="0 -15 256 256"
version="1.1"
Expand Down
2 changes: 1 addition & 1 deletion examples/svg/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ const observer = new PerformanceObserver((list) => {
});

observer.observe({
entryTypes: ["element", "paint", "largest-contentful-paint"],
entryTypes: ["element", "paint", "largest-contentful-paint", "container"],
buffered: true,
});
4 changes: 2 additions & 2 deletions examples/table/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
window.ctDebug = true;
const observer = new ContainerPerformanceObserver((list) => {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(entry);
});
});

observer.observe({ method: "newAreaPainted" });
observer.observe({ type: "container" });

window.setTimeout(() => {
document.querySelectorAll(".dynupdate").forEach((elm) => {
Expand Down
175 changes: 134 additions & 41 deletions polyfill/polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,14 @@ interface PerformanceElementTiming extends PerformanceEntry {
size: number;
}

interface PerformanceContainerTiming extends PerformanceEntry {
firstRenderTime: number;
identifier: string | null;
lastPaintedSubElement?: Element | null;
size: number;
startTime: number;
}
interface ResolvedRootData extends PerformanceContainerTiming {
/** Keep track of all the paintedRects */
paintedRects: Set<DOMRectReadOnly>;
/** For aggregated paints keep track of the union painted rect */
coordData?: any;
}

type ObserveOptions = {
nestedStrategy: "ignore" | "transparent" | "shadowed";
};
type NestedStrategy = "ignore" | "transparent" | "shadowed";

// We need to set the element timing attribute tag on all elements below "containertiming" before we can start observing
// Otherwise no elements will be observed
Expand All @@ -46,6 +37,7 @@ let lastResolvedData: Partial<{
intersectionRect: DOMRectReadOnly;
}>;

let mutationObserver;
const mutationObserverCallback = (mutationList: MutationRecord[]) => {
const findContainers = (parentNode: Element) => {
// We've found a container
Expand Down Expand Up @@ -92,9 +84,7 @@ const mutationObserverCallback = (mutationList: MutationRecord[]) => {

// Wait until the DOM is ready then start collecting elements needed to be timed.
document.addEventListener("DOMContentLoaded", () => {
const mutationObserver = new window.MutationObserver(
mutationObserverCallback,
);
mutationObserver = new window.MutationObserver(mutationObserverCallback);

const config = { attributes: false, childList: true, subtree: true };
mutationObserver.observe(document, config);
Expand All @@ -106,24 +96,53 @@ document.addEventListener("DOMContentLoaded", () => {
});
});

class PerformanceContainerTiming implements PerformanceEntry {
entryType = "container";
name = "";
duration = 0;
startTime: number;
identifier: string | null;
firstRenderTime: number;
size: number;
lastPaintedElement: Element | null;

constructor(
startTime: number,
identifier: string | null,
size: number,
firstRenderTime: number,
lastPaintedElement: Element | null,
) {
this.identifier = identifier;
this.size = size;
this.startTime = startTime;
this.firstRenderTime = firstRenderTime;
this.lastPaintedElement = lastPaintedElement;
}

toJSON(): void {}
}

/**
* Container Performance Observer is a superset of Performance Observer which can augment element-timing to work on containers
*/
class ContainerPerformanceObserver {
class ContainerPerformanceObserver implements PerformanceObserver {
nativePerformanceObserver: PerformanceObserver;
// Debug flag to show overlays
debug: boolean;
nestedStrategy: ObserveOptions["nestedStrategy"] = "ignore";
callback: (list: {
getEntries: () => PerformanceContainerTiming[];
}) => PerformanceContainerTiming[];
// Which nested strategy is set
nestedStrategy: NestedStrategy = "ignore";
// We need to know if element timing has been explicitly set or not
overrideElementTiming: boolean = false;
// is container timing being used or should we just passthrough to the native polyfill
polyfillEnabled: boolean = false;
// We need to keep track of set entryTypes so we know whether to ignore element timing
entryTypes: string[] = [];
callback: PerformanceObserverCallback;

static supportedEntryTypes = NativePerformanceObserver.supportedEntryTypes;

constructor(
callback: (list: {
getEntries: () => PerformanceContainerTiming[];
}) => PerformanceContainerTiming[],
) {
constructor(callback: PerformanceObserverCallback) {
this.nativePerformanceObserver = new NativePerformanceObserver(
this.callbackWrapper.bind(this),
);
Expand Down Expand Up @@ -183,6 +202,7 @@ class ContainerPerformanceObserver {
size: 0,
startTime: 0,
firstRenderTime: 0,
lastPaintedElement: null,
toJSON: () => {},
};

Expand Down Expand Up @@ -230,9 +250,51 @@ class ContainerPerformanceObserver {
}, 2000);
}

observe(options: ObserveOptions) {
this.nestedStrategy = options.nestedStrategy;
this.nativePerformanceObserver.observe({ type: "element", buffered: true });
takeRecords(): PerformanceEntryList {
const list = this.nativePerformanceObserver.takeRecords();

// Don't expose element timing records if the user didn't ask for them
if (this.overrideElementTiming) {
return list.filter((entry) => entry.entryType !== "element");
}

return list;
}

observe(
options?: PerformanceObserverInit & { nestedStrategy: NestedStrategy },
) {
const hasOption = (name: string, options?: PerformanceObserverInit) =>
options?.entryTypes?.includes(name) || options?.type === name;

if (hasOption("container", options)) {
this.polyfillEnabled = true;
let resolvedTypes = options?.type
? [options?.type]
: options?.entryTypes ?? [];

// Remove "container" before passing down into PerfObserver
resolvedTypes = resolvedTypes.filter((type) => type !== "container");

if (!hasOption("element", options)) {
this.overrideElementTiming = true;
resolvedTypes = resolvedTypes.concat("element");
}

this.entryTypes = resolvedTypes;
this.nestedStrategy ??= options?.nestedStrategy || "ignore";
// If we only have 1 type its preferred to use the type property, otherwise use entryTypes
// This is to make sure buffered still works when we only have "elmeent" set.
this.nativePerformanceObserver.observe({
type: resolvedTypes.length === 1 ? resolvedTypes[0] : undefined,
entryTypes: resolvedTypes.length > 1 ? resolvedTypes : undefined,
buffered: resolvedTypes.length === 1 ? true : undefined,
});
return;
}

// We're just using the observer as normal
this.nativePerformanceObserver.observe(options);
}

disconnect() {
Expand Down Expand Up @@ -265,8 +327,8 @@ class ContainerPerformanceObserver {
}
}

resolvedRootData.lastPaintedSubElement = entry.element;
resolvedRootData.startTime = entry.renderTime;
resolvedRootData.lastPaintedElement = entry.element;
resolvedRootData.startTime = entry.startTime; // For images this will either be the load time or render time
resolvedRootData.paintedRects?.add(entry.intersectionRect);
// size won't be super accurate as it doesn't take into account overlaps
resolvedRootData.size += incomingEntrySize;
Expand Down Expand Up @@ -337,6 +399,15 @@ class ContainerPerformanceObserver {
return rect.width * rect.height;
}

// This polyfill uses element timing, but we don't leak that back to the user unless intended
filterEntryList(list: PerformanceEntryList): PerformanceEntryList {
if (this.overrideElementTiming) {
return list.filter((entry) => entry.entryType !== "element");
} else {
return list;
}
}

/**
* This will wrap the callback and add extra fields for container elements
* @param {PerformanceObserverEntryList} list
Expand Down Expand Up @@ -380,16 +451,13 @@ class ContainerPerformanceObserver {
if (!resolvedRootData) {
return;
}
const containerCandidate: any = {
entryType: "container",
name: "",
startTime: resolvedRootData.startTime,
identifier: resolvedRootData.identifier,
duration: 0,
firstRenderTime: resolvedRootData.firstRenderTime,
size: resolvedRootData.size,
lastPaintedSubElement: resolvedRootData.lastPaintedSubElement,
};
const containerCandidate: any = new PerformanceContainerTiming(
resolvedRootData.startTime,
resolvedRootData.identifier,
resolvedRootData.size,
resolvedRootData.firstRenderTime,
resolvedRootData.lastPaintedElement,
);

containerEntries.push(containerCandidate);

Expand All @@ -405,10 +473,35 @@ class ContainerPerformanceObserver {
list.getEntries().forEach(processEntries);
const containers = fetchUpdatedContainers();

const syntheticList = {
getEntries: () => containers,
const syntheticList: PerformanceObserverEntryList = {
getEntries: () => {
const defaultEntries = this.filterEntryList(list.getEntries());
return defaultEntries.concat(containers);
},
getEntriesByName: (name, type) => {
const defaultEntries = this.filterEntryList(
list.getEntriesByName(name, type),
);
if (type === "container") {
defaultEntries.concat(containers);
}

return defaultEntries;
},
getEntriesByType: (type) => {
const defaultEntries = this.filterEntryList(
list.getEntriesByType(type),
);
if (type === "container") {
defaultEntries.concat(containers);
}

return defaultEntries;
},
};

this.callback(syntheticList);
this.callback(syntheticList, this);
}
}

window.PerformanceObserver = ContainerPerformanceObserver;

0 comments on commit aa3d727

Please sign in to comment.