From afe6ec9dd13ddf3cb81bf57f1d853fbddb1bd7d9 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] Compact style mutation (#464) * Don't store the full style attribute change, as small mutations to single style properties result in storage of a rewrite for the full style attribute, which may be very large. Had an example of a website using http://schillmania.com/projects/snowstorm/ where many direct style changes were happening every second across many 'snowflake' elements, with each attribute change looking like: "style":"color: rgb(255, 255, 255); position: absolute; width: 8px; height: 8px; font-family: arial, verdana; overflow: hidden; font-weight: normal; z-index: 0; display: block; bottom: auto; opacity: 1; padding: 0px; margin: 0px; font-size: 10px; line-height: 10px; text-align: center; vertical-align: baseline; left: 242.807px; top: 85.7332px;" even though maybe just the left/top position had been changed * More compact storage for the much more common attribute value without an `!important` flag - saves 6 chars per style attr in the json :) * Fix bug: attributes weren't getting removed after changes to treatment of 'style' attributes --- src/record/mutation.ts | 44 +++++++++++++++++---- src/replay/index.ts | 30 +++++++++----- src/types.ts | 8 +++- test/__snapshots__/integration.test.ts.snap | 36 ++++++++++++++++- test/integration.test.ts | 3 +- test/utils.ts | 23 +++++++---- 6 files changed, 114 insertions(+), 30 deletions(-) diff --git a/src/record/mutation.ts b/src/record/mutation.ts index 04f0065b..2344265c 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -449,6 +449,7 @@ export default class MutationBuffer { break; } case 'attributes': { + const target = (m.target as HTMLElement); let value = (m.target as HTMLElement).getAttribute(m.attributeName!); if (m.attributeName === 'value') { value = maskInputValue({ @@ -472,13 +473,42 @@ export default class MutationBuffer { }; this.attributes.push(item); } - // overwrite attribute if the mutations was triggered in same time - item.attributes[m.attributeName!] = transformAttribute( - this.doc, - (m.target as HTMLElement).tagName, - m.attributeName!, - value!, - ); + if (m.attributeName === 'style') { + const old = this.doc.createElement('span'); + old.setAttribute('style', m.oldValue); + if (item.attributes['style'] === undefined) { + item.attributes['style'] = {}; + } + for (let i=0; i { - if ( - 'style' in a.attributes && - coordinatesReg.test(a.attributes.style!) - ) { - a.attributes.style = a.attributes.style!.replace( - coordinatesReg, - '$1: Npx', - ); + if ('style' in a.attributes && a.attributes.style && typeof a.attributes.style === 'object') { + for (const [k, v] of Object.entries(a.attributes.style)) { + if (Array.isArray(v)) { + if (coordinatesReg.test(k + ': ' + v[0])) { + // TODO: could round the number here instead depending on what's coming out of various test envs + a.attributes.style[k] = ['Npx', v[1]]; + } + } else if (typeof v === 'string') { + if (coordinatesReg.test(k + ': ' + v)) { + a.attributes.style[k] = 'Npx'; + } + } + coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript + } } }); s.data.adds.forEach((add) => { @@ -97,6 +103,7 @@ function stringifySnapshots(snapshots: eventWithTime[]): string { '$1: Npx', ); } + coordinatesReg.lastIndex = 0; // wow, a real wart in ECMAScript }); } delete s.timestamp;