Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 102 additions & 30 deletions packages/browser/src/global/stabilization/plugins/loadImageSrcset.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,132 @@
import type { Plugin } from "..";

/**
* Force the reload of srcset on resize.
* To ensure that if the viewport changes, it's the same behaviour
* as if the page was reloaded.
* Force srcset to resolve to the biggest candidate for the current run.
* This collapses srcset to a single candidate while staying consistent with
* descriptor rules from the spec.
*/
export const plugin = {
name: "loadImageSrcset" as const,
beforeEach(options) {
// If the user is not using viewports, do nothing.
if (!options.viewports || options.viewports.length === 0) {
return undefined;
}

function getLargestSrcFromSrcset(srcset: string) {
// Parse srcset into array of {url, width}
const sources = srcset
type ParsedCandidate =
| { url: string; kind: "w"; value: number }
| { url: string; kind: "x"; value: number }
| { url: string; kind: "none"; value: 1 };

function parseSrcset(srcset: string): ParsedCandidate[] {
return srcset
.split(",")
.map((item) => {
const [url, size] = item.trim().split(/\s+/);
if (!url) {
return null;
.reduce<ParsedCandidate[]>((candidates, rawPart) => {
const part = rawPart.trim();
if (!part) {
return candidates;
}

const tokens = part.split(/\s+/);
const maybeDescriptor =
tokens.length > 1 ? tokens[tokens.length - 1] : null;

if (maybeDescriptor && /^\d+w$/.test(maybeDescriptor)) {
const url = tokens.slice(0, -1).join(" ");
if (url) {
candidates.push({
url,
kind: "w",
value: Number.parseInt(maybeDescriptor.slice(0, -1), 10),
});
}
return candidates;
}
// Only handle width descriptors (e.g., 800w)
const widthMatch = size && size.match(/^(\d+)w$/);
if (!widthMatch) {
return { url, width: 0 };

if (maybeDescriptor && /^\d+\.?\d*x$/.test(maybeDescriptor)) {
const url = tokens.slice(0, -1).join(" ");
if (url) {
candidates.push({
url,
kind: "x",
value: Number.parseFloat(maybeDescriptor.slice(0, -1)),
});
}
return candidates;
}

const url = tokens[0] ?? "";
if (url) {
candidates.push({ url, kind: "none", value: 1 });
}
const width = parseInt(widthMatch[1]!, 10);
return { url, width };
})
.filter((x) => x !== null);

if (sources.length === 0) {
return srcset;
return candidates;
}, []);
}

function pickLargestCandidate(
candidates: ParsedCandidate[],
): ParsedCandidate | null {
let winner: ParsedCandidate | null = null;
let kind: ParsedCandidate["kind"] | null = null;

for (const candidate of candidates) {
if (kind !== null && candidate.kind !== kind) {
return null;
}

kind ??= candidate.kind;

if (winner === null || candidate.value > winner.value) {
winner = candidate;
}
}

// Find the source with the largest width
const largest = sources.reduce((max, curr) =>
curr.width > max.width ? curr : max,
);
return winner;
}

// Return only the largest source as srcset
return largest.url;
function candidateToSingleSrcset(
candidate: ParsedCandidate,
requireW: boolean,
): string {
if (candidate.kind === "none") {
return requireW ? `${candidate.url} 1w` : candidate.url;
}

return `${candidate.url} ${candidate.value}${candidate.kind}`;
}

function forceSrcsetReload(img: Element) {
const srcset = img.getAttribute("srcset");
function forceSrcsetReload(el: Element): void {
const srcset = el.getAttribute("srcset");
if (!srcset) {
return;
}
img.setAttribute("srcset", getLargestSrcFromSrcset(srcset));

const candidates = parseSrcset(srcset);
const chosen = pickLargestCandidate(candidates);
if (!chosen) {
return;
}

const requireW =
el.hasAttribute("sizes") ||
chosen.kind === "w" ||
candidates.some((c) => c.kind === "w");

el.setAttribute("srcset", "");

if (el instanceof HTMLImageElement) {
el.src = chosen.url;
}

void el.clientWidth;

el.setAttribute("srcset", candidateToSingleSrcset(chosen, requireW));
}

Array.from(document.querySelectorAll("img,source")).forEach(
forceSrcsetReload,
);

return undefined;
},
} satisfies Plugin;
23 changes: 11 additions & 12 deletions packages/browser/src/global/stabilization/plugins/roundImageSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,33 @@ export const plugin = {
name: "roundImageSize" as const,
beforeEach() {
Array.from(document.images).forEach((img) => {
// Skip images that are not loaded yet.
if (!img.complete) {
if (!img.complete || img.naturalWidth === 0) {
return;
}

// Backup the original width and height
img.setAttribute(BACKUP_ATTRIBUTE_WIDTH, img.style.width);
img.setAttribute(BACKUP_ATTRIBUTE_HEIGHT, img.style.height);
// Backup only once
if (!img.hasAttribute(BACKUP_ATTRIBUTE_WIDTH)) {
img.setAttribute(BACKUP_ATTRIBUTE_WIDTH, img.style.width);
img.setAttribute(BACKUP_ATTRIBUTE_HEIGHT, img.style.height);
}

// Set the width and height to the rounded values
img.style.width = `${Math.round(img.offsetWidth)}px`;
img.style.height = `${Math.round(img.offsetHeight)}px`;
});

return () => {
Array.from(document.images).forEach((img) => {
const bckWidth = img.getAttribute(BACKUP_ATTRIBUTE_WIDTH);
const bckHeight = img.getAttribute(BACKUP_ATTRIBUTE_HEIGHT);

if (bckWidth === null && bckHeight === null) {
// Only restore if we actually backed up this image
if (!img.hasAttribute(BACKUP_ATTRIBUTE_WIDTH)) {
return;
}

// Restore the original width and height
const bckWidth = img.getAttribute(BACKUP_ATTRIBUTE_WIDTH);
const bckHeight = img.getAttribute(BACKUP_ATTRIBUTE_HEIGHT);

img.style.width = bckWidth ?? "";
img.style.height = bckHeight ?? "";

// Remove the backup attributes
img.removeAttribute(BACKUP_ATTRIBUTE_WIDTH);
img.removeAttribute(BACKUP_ATTRIBUTE_HEIGHT);
});
Expand Down
17 changes: 3 additions & 14 deletions packages/browser/src/global/stabilization/plugins/waitForImages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ import type { Plugin } from "..";
export const plugin = {
name: "waitForImages" as const,
beforeEach() {
Array.from(document.images).every((img) => {
// Force sync decoding
Array.from(document.images).forEach((img) => {
if (img.decoding !== "sync") {
img.decoding = "sync";
}

// Force eager loading
if (img.loading !== "eager") {
img.loading = "eager";
}
Expand All @@ -21,23 +18,15 @@ export const plugin = {
},
wait: {
for: () => {
const images = Array.from(document.images);

const results = images.map((img) => {
// Force sync decoding
return Array.from(document.images).every((img) => {
if (img.decoding !== "sync") {
img.decoding = "sync";
}

// Force eager loading
if (img.loading !== "eager") {
img.loading = "eager";
}

return img.complete;
return img.complete && img.naturalWidth > 0;
});

return results.every((x) => x);
},
failureExplanation: "Some images have not been loaded",
},
Expand Down
Loading