Last active
January 19, 2026 12:35
-
-
Save kurtextrem/038aa36e504485381a78e518dfc6a600 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * DevTools snippet – find potential stutter sources | |
| * Run in the Chrome DevTools console on any page | |
| * Flags: | |
| * 1. Elements > 1000×1000 px that have any transform-* or opacity set | |
| * 2. Elements that have any filter applied | |
| * If at least one of each category exists → we have a match. | |
| * Matching elements get a red 3 px outline and a console table. | |
| */ | |
| (() => { | |
| const TRANSFORM_PROPS = [ | |
| 'transform','transformOrigin','transformStyle','perspective','backfaceVisibility', | |
| 'translate','rotate','scale','opacity','willChange' | |
| ]; | |
| const FILTER_PROPS = ['filter']; | |
| const MAYBE_HEAVY_PROPS = ['backdropFilter', 'boxShadow']; | |
| const CRITICAL_FILTERS = ["contrast", "blur"]; | |
| const IGNORE_EL = ["html", "body", "div#main"]; | |
| const DEFAULTS = { | |
| transform: 'none', | |
| transformOrigin: '50% 50%', | |
| transformStyle: 'flat', | |
| perspective: 'none', | |
| backfaceVisibility: 'visible', | |
| translate: 'none', | |
| rotate: 'none', | |
| scale: 'none', | |
| opacity: '1', | |
| willChange: 'auto', | |
| filter: 'none', | |
| backdropFilter: 'none', | |
| boxShadow: 'none' | |
| }; | |
| /* ---------- helpers ---------- */ | |
| const style = el => window.getComputedStyle(el); | |
| const pickProps = (obj, keys) => { | |
| const out = {}; | |
| keys.forEach(k => { | |
| const v = obj[k]; | |
| if (!v) return; | |
| const defaultVal = DEFAULTS[k]; | |
| if (v === defaultVal) return; | |
| if (v === 'none' || v === 'normal') return; | |
| out[k] = v; | |
| }); | |
| return out; | |
| }; | |
| const hasAny = o => Object.keys(o).length > 0; | |
| /* ---------- filter-value checker ---------- */ | |
| const filterRegex = /[a-z-]+\([^)]+\)/gi; | |
| const splitRegex = /\(|\)/; | |
| const isMeaningfulFilter = val => { | |
| if (!val || val === 'none') return false; | |
| if (val.includes('var(--')) return true; | |
| const parts = val.match(filterRegex) || []; | |
| return parts.some(p => { | |
| const [fn, rawVal] = p.split(splitRegex).filter(Boolean); | |
| const v = parseFloat(rawVal); | |
| switch (fn.trim()) { | |
| case 'brightness': | |
| case 'contrast': | |
| case 'saturate': | |
| case 'sepia': | |
| case 'grayscale': return v !== 1 && v !== 100; | |
| case 'invert': return v !== 0; | |
| case 'opacity': return v !== 1 && v !== 100; | |
| case 'blur': return v !== 0; | |
| case 'hue-rotate': return v !== 0; | |
| case 'drop-shadow': return true; | |
| default: return true; | |
| } | |
| }); | |
| }; | |
| /* ---------- scan ---------- */ | |
| const bigTransforms = []; | |
| const withFilters = []; | |
| const maybeHeavy = []; | |
| document.querySelectorAll('*').forEach(el => { | |
| if (IGNORE_EL.includes(`${el.tagName.toLowerCase()}${el.id?'#'+el.id:''}`)) return; | |
| const r = el.getBoundingClientRect(); | |
| const s = style(el); | |
| if (r.width > 1000 && r.height > 1000) { | |
| const tProps = pickProps(s, TRANSFORM_PROPS); | |
| if (hasAny(tProps)) bigTransforms.push({el, r, props: tProps}); | |
| } | |
| const fProps = pickProps(s, FILTER_PROPS); | |
| Object.keys(fProps).forEach(k => { | |
| if (!isMeaningfulFilter(fProps[k])) delete fProps[k]; | |
| }); | |
| if (hasAny(fProps)) withFilters.push({el, r, props: fProps}); | |
| const mProps = pickProps(s, MAYBE_HEAVY_PROPS); | |
| if (hasAny(mProps)) maybeHeavy.push({el, r, props: mProps}); | |
| }); | |
| /* ---------- outline helpers ---------- */ | |
| const OUTLINE = '3px solid red'; | |
| const KEY = '__stutterOutline'; | |
| const highlight = arr => arr.forEach(({el}) => { | |
| el[KEY] = el.style.outline; | |
| el.style.outline = OUTLINE; | |
| }); | |
| const unhighlight = arr => arr.forEach(({el}) => { | |
| el.style.outline = el[KEY] || ''; | |
| delete el[KEY]; | |
| }); | |
| /* ---------- report ---------- */ | |
| if (bigTransforms.length && withFilters.length) { | |
| console.group(`%c⚠️ Match found! Marked in red are elements that are larger than the viewport.`, 'color:orange;font-weight:bold;'); | |
| const viewportWidth = window.innerWidth; | |
| const viewportHeight = window.innerHeight; | |
| console.groupCollapsed(`Big-transform elements (${bigTransforms.length})`); | |
| bigTransforms.sort((a,b) => b.r.width - a.r.width).forEach(({el, r, props}) => { | |
| console.groupCollapsed( | |
| `%c${el.tagName.toLowerCase()}${el.id?'#'+el.id:''}${el.className?'.'+el.className.split(' ').join('.'):''} ${el.dataset.framerName ? el.dataset.framerName+") " : ''}| %c${r.width}x${r.height}`, | |
| 'font-weight:bold', | |
| r.width > viewportWidth || r.height > viewportHeight ? 'color:red' : '' | |
| ); | |
| console.log('Node →', el); | |
| console.log('Rect →', r.width, 'x', r.height, r); | |
| console.log('Props →', props); | |
| console.groupEnd(); | |
| }); | |
| console.groupEnd(); | |
| console.groupCollapsed(`Elements with filter (${withFilters.length})`); | |
| withFilters.sort((a,b) => a.props.filter.localeCompare(b.props.filter)).forEach(({el, props}) => { | |
| console.groupCollapsed( | |
| `%c${el.tagName.toLowerCase()}${el.id?'#'+el.id:''}${el.className?'.'+el.className.split(' ').join('.'):''} | %c${props.filter}`, | |
| 'font-weight:bold', | |
| CRITICAL_FILTERS.some(f => props.filter.includes(f)) ? 'color:red' : '' | |
| ); | |
| console.log('Node →', el); | |
| console.log('Props →', props); | |
| console.groupEnd(); | |
| }); | |
| console.groupEnd(); | |
| console.groupCollapsed(`Elements with maybe heavy props (${maybeHeavy.length})`); | |
| const groupedByProp = {}; | |
| maybeHeavy.forEach(({el, props}) => { | |
| Object.keys(props).forEach(prop => { | |
| if (!groupedByProp[prop]) groupedByProp[prop] = []; | |
| groupedByProp[prop].push({el, props}); | |
| }); | |
| }); | |
| Object.keys(groupedByProp).sort().forEach(prop => { | |
| const elements = groupedByProp[prop]; | |
| console.groupCollapsed(`${prop} (${elements.length})`); | |
| elements.forEach(({el, props}) => { | |
| console.groupCollapsed( | |
| `%c${el.tagName.toLowerCase()}${el.id?'#'+el.id:''}${el.className?'.'+el.className.split(' ').join('.'):''} | %c${props.backdropFilter}`, | |
| 'font-weight:bold', | |
| CRITICAL_FILTERS.some(f => props.backdropFilter.includes(f)) ? 'color:red' : '' | |
| ); | |
| console.log('Node →', el); | |
| console.log('Props →', props); | |
| console.groupEnd(); | |
| }); | |
| console.groupEnd(); | |
| }); | |
| console.groupEnd(); | |
| highlight([...bigTransforms, ...withFilters, ...maybeHeavy]); | |
| window.unhighlightStutter = () => { | |
| unhighlight([...bigTransforms, ...withFilters, ...maybeHeavy]); | |
| console.log('Outlines removed.'); | |
| }; | |
| console.log('%cRun `unhighlightStutter()` to remove outlines.','color:steelblue'); | |
| } else { | |
| console.log('%c✅ No stutter candidates found.','color:green;font-weight:bold'); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment