Skip to content

Commit

Permalink
Add a queueing mechanism so that in Live Mode we don't render full sn…
Browse files Browse the repository at this point in the history
…apshots until we receive the stylesheet assets to avoid a flash of unstyled content (fouc)
  • Loading branch information
eoghanmurray committed Aug 23, 2024
1 parent 182cc76 commit 93cb523
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 23 deletions.
31 changes: 25 additions & 6 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,9 @@ function record<T = eventWithTime>(

shadowDomManager.init();

let liveBuffer = 0;
let assetCount = 0;

mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting
const node = snapshot(document, {
mirror,
Expand Down Expand Up @@ -444,22 +447,38 @@ function record<T = eventWithTime>(
stylesheetManager.attachLinkElement(linkEl, childSn);
},
onAssetDetected: (asset: asset) => {
assetManager.capture(asset);
const assetStatus = assetManager.capture(asset);
if (
'timeout' in assetStatus && // removeme when we just capture one asset from srcset
assetStatus.timeout
) {
// currently only stylesheet assets return a timeout
// indicating that we want the fullsnapshot to wait in order to avoid a flash of unstyled content
liveBuffer = Math.max(
liveBuffer,
assetStatus.timeout + 100, // add a guess for worst case processing time
);
}
assetCount += 1;
},
keepIframeSrcFn,
});

if (!node) {
return console.warn('Failed to snapshot the document');
}

const data = {
node,
initialOffset: getWindowScroll(window),
};
if (liveBuffer > 0) {
data.liveBuffer = liveBuffer;
data.assetCount = assetCount;
}
wrappedEmit(
{
type: EventType.FullSnapshot,
data: {
node,
initialOffset: getWindowScroll(window),
},
data,
},
isCheckout,
);
Expand Down
10 changes: 5 additions & 5 deletions packages/rrweb/src/record/observers/asset-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,7 @@ export default class AssetManager {
}
const processStylesheet = () => {
cssRules = el.sheet!.cssRules; // update, as a mutation may have since occurred
const cssText = stringifyCssRules(
cssRules,
sheetBaseHref,
);
const cssText = stringifyCssRules(cssRules, sheetBaseHref);
const payload: SerializedCssTextArg = {
rr_type: 'CssText',
cssTexts: [cssText],
Expand Down Expand Up @@ -220,7 +217,10 @@ export default class AssetManager {
requestIdleCallback(processStylesheet, {
timeout,
});
return { status: 'capturing' }; // 'processing' ?
return {
status: 'capturing', // 'processing' ?
timeout,
};
} else {
processStylesheet();
return { status: 'captured' };
Expand Down
62 changes: 50 additions & 12 deletions packages/rrweb/src/replay/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export function createPlayerService(
context: PlayerContext,
{ getCastFn, applyEventsSynchronously, emitter }: PlayerAssets,
) {
const addEventQueue: [Event] = [];
const addEventQueueTimeout: ReturnType<typeof setTimeout> | -1;
const addEventQueueAssetCount = -1;

const playerMachine = createMachine<PlayerContext, PlayerEvent, PlayerState>(
{
id: 'player',
Expand Down Expand Up @@ -236,7 +240,7 @@ export function createPlayerService(
},
}),
addEvent: assign((ctx, machineEvent) => {
const { baselineTime, timer, events } = ctx;
const { events } = ctx;
if (machineEvent.type === 'ADD_EVENT') {
const { event } = machineEvent.payload;
addDelay(event, baselineTime);
Expand All @@ -262,17 +266,51 @@ export function createPlayerService(
events.splice(insertionIndex, 0, event);
}

const isSync = event.timestamp < baselineTime;
const castFn = getCastFn(event, isSync);
if (isSync) {
castFn();
} else if (timer.isActive()) {
timer.addAction({
doAction: () => {
castFn();
},
delay: event.delay!,
});
const castOrScheduleEvent = (event) => {
const { baselineTime, timer } = ctx;
const isSync = event.timestamp < baselineTime;
const castFn = getCastFn(event, isSync);
if (isSync) {
castFn();
} else if (timer.isActive()) {
timer.addAction({
doAction: () => {
castFn();
},
delay: event.delay!,
});
}
};

const flushAddEventQueue = () => {
addEventQueueTimeout = -1;
while (addEventQueue.length) {
castOrScheduleEvent(addEventQueue.shift());
}
};

if (event.type === EventType.Asset && addEventQueueTimeout) {
addEventQueueAssetCount -= 1;
}
if (addEventQueue.length) {
addEventQueue.push(event);
// TODO: support appearance of a second FullSnapshot before first one's assets load
if (addEventQueueAssetCount <= 0) {
clearTimeout(addEventQueueTimeout);
this.flushAddEventQueue();
}
} else if (
event.type === EventType.FullSnapshot &&
event.assetCount
) {
addEventQueue.push(event);
addEventQueueAssetCount = event.assetCount;
addEventQueueTimeout = setTimeout(
flushAddEventQueue,
event.liveBuffer,
);
} else {
castOrScheduleEvent(event);
}
}
return { ...ctx, events };
Expand Down
1 change: 1 addition & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export type ErrorHandler = (error: unknown) => void | boolean;

export type assetStatus = {
status: 'capturing' | 'captured' | 'error' | 'refused';
timeout?: number;
};

export interface ProcessingStyleElement extends HTMLStyleElement {
Expand Down
2 changes: 2 additions & 0 deletions packages/rrweb/test/events/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ const events: eventWithTime[] = [
id: 1,
},
initialOffset: { left: 0, top: 0 },
liveBuffer: 50,
liveBufferAssetCount: 3,
},
timestamp: 1636379531389,
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions packages/rrweb/test/replay/asset-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ describe('replayer', function () {
expect(image).toMatchImageSnapshot();
});

it('should wait for stylesheet assets to avoid fouc', async () => {
// fouc = flash of unstyled content
await page.evaluate(`
const { Replayer } = rrweb;
window.replayer = new Replayer([], {
liveMode: true,
});
replayer.startLive();
window.replayer.addEvent(events[0]);
window.replayer.addEvent(events[1]);
window.replayer.addEvent(events[2]);
`);

await waitForRAF(page);
const image = await page.screenshot();
expect(image).toMatchImageSnapshot(); // should be blank white and not have image rendered yet
});

it('should support urls src modified via incremental mutation', async () => {
await page.evaluate(`
const { Replayer } = rrweb;
Expand Down
11 changes: 11 additions & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ export type fullSnapshotEvent = {
top: number;
left: number;
};
/*
* in milliseconds, how long we should delay rebuild
* wait in a live context in order that assets can be transmitted
*/
liveBuffer?: number;
/*
* the number of assets associated with this snapshot
* useful for processing streams of events without having
* to rebuild this event to count them up
*/
assetCount?: number;
};
};

Expand Down

0 comments on commit 93cb523

Please sign in to comment.