This guide helps you verify that virtual scrolling works correctly and delivers real performance improvements.
- Chrome browser with Developer mode enabled
- ChatGPT account
- A long conversation (100+ messages) OR ability to create one
- Chrome DevTools familiarity
-
Load Extension
chrome://extensions/ → Enable Developer mode → Load unpacked → Select Trimwise folder -
Verify Installation
- Extension icon appears in toolbar
- Right-click icon → Options opens settings page
- No console errors in extension page
Objective: Verify extension loads and applies visibility rules
Steps:
- Open ChatGPT (https://chatgpt.com/)
- Open a conversation with 50+ messages
- Open DevTools → Console
- Look for:
[Trimwise] Initializing v2.0 with virtual scrolling - Scroll to bottom of conversation
Expected Results:
- ✅ Console shows initialization message
- ✅ "Show more" button appears near top
- ✅ Button shows count like "Show 20 more messages (30 hidden)"
- ✅ Only recent messages visible
- ✅ No console errors
Failure Modes:
- ❌ No console message → Extension not loading
- ❌ No button → Check
startObserving()found<main>element - ❌ All messages visible → Check
applyVisibilityRules()
Objective: Verify messages actually removed from DOM (not just hidden)
Steps:
- Open ChatGPT conversation with 200+ messages
- Open DevTools → Elements tab
- Find a message article element (search for
data-testid="conversation-turn") - Right-click → Scroll into view
- Scroll far away from that message (1000+ px)
- Wait 2 seconds
- Find that message in Elements tab again
Expected Results:
- ✅ Message replaced with
<div class="trimwise-placeholder"> - ✅ Placeholder has inline
heightstyle matching original - ✅ Console shows:
[Trimwise] Virtualized message X, height: Ypx - ✅ Original message NOT in DOM (search finds placeholder only)
Failure Modes:
- ❌ Message still exists with
display: none→ Virtual scrolling not working - ❌ No placeholder → Check
virtualizeMessage()logic - ❌ Placeholder wrong height → Check
getBoundingClientRect()timing
Debug Commands (in Console):
// Check how many messages virtualized
console.log('Virtualized:', virtualizedMessages.size);
// Check all placeholders
document.querySelectorAll('.trimwise-placeholder').length;
// Check all real messages
document.querySelectorAll('article[data-testid^="conversation-turn-"]').length;Objective: Verify removed messages restore when scrolling back
Steps:
- Continue from Test 2 (message virtualized)
- Note the placeholder's position
- Slowly scroll back toward the placeholder
- Stop when placeholder is ~1500px away (still offscreen)
- Wait 1 second
- Check Elements tab
Expected Results:
- ✅ Placeholder replaced with original message article
- ✅ Console shows:
[Trimwise] Restored message X - ✅ Message content looks identical (no re-render artifacts)
- ✅ Scroll position unchanged (no jump)
Failure Modes:
- ❌ Placeholder still there when scrolling → Check
placeholderObserverthresholds - ❌ Message pops in suddenly → Buffer zone too small (increase
rootMargin) - ❌ Scroll jumps → Placeholder height wrong
- ❌ Message content broken → Cache corruption
Debug Commands:
// Check observer setup
console.log('Message observer:', messageObserver);
console.log('Placeholder observer:', placeholderObserver);
// Force restore all
virtualizedMessages.forEach((cached, article) => {
console.log('Cached message:', cached.index, cached.height);
});Objective: Verify no scroll jumping during virtualization
Steps:
- Open conversation with 300+ messages
- Scroll to middle of conversation
- Find a recognizable message (e.g., contains specific word)
- Note its exact position on screen
- Scroll away 2000px
- Scroll back to that message
Expected Results:
- ✅ Message appears at same screen position
- ✅ No visible "pop" or "jump"
- ✅ Smooth scrolling throughout
- ✅ Adjacent messages maintain spacing
Failure Modes:
- ❌ Scroll jumps down → Placeholder height too small
- ❌ Scroll jumps up → Placeholder height too large
- ❌ Spacing wrong → CSS issues or margin calculation
Measurement (in Console):
// Before scroll:
const msg = document.querySelector('article[data-testid="conversation-turn-5"]');
const beforeY = msg.getBoundingClientRect().top;
console.log('Before Y:', beforeY);
// After scroll away and back:
const msg2 = document.querySelector('article[data-testid="conversation-turn-5"]');
const afterY = msg2.getBoundingClientRect().top;
console.log('After Y:', afterY);
console.log('Diff:', Math.abs(afterY - beforeY), 'px'); // Should be < 1pxObjective: Verify button expands visible range correctly
Steps:
- Open conversation with 100+ messages
- Scroll to bottom
- Set batch size to 20 (in options)
- Reload ChatGPT
- Click "Show more" button
Expected Results:
- ✅ Button text updates: "Show 20 more messages (X hidden)"
- ✅ Hidden count decreases by 20
- ✅ 20 additional messages become visible
- ✅ Button moves up to new position
- ✅ No page scroll (stays at same position)
Special Case - Click until all shown:
- ✅ Button text changes to "All messages are shown"
- ✅ Clicking button resets to initial state (only 20 visible)
Failure Modes:
- ❌ Button doesn't update → Check
updateShowMoreButton()change detection - ❌ Wrong count → Check
currentOffsetcalculation - ❌ Button in wrong position → Check
insertBefore()logic
Objective: Verify real memory reduction vs v1.1 hide approach
Steps:
- Open ChatGPT with NEW conversation
- Open DevTools → Memory tab
- Click "Take snapshot" (baseline)
- Generate 200 messages (copy/paste, multi-line questions, repeat)
- Wait for all responses
- Take snapshot (with extension)
- Disable extension
- Reload page, repeat steps 4-5
- Take snapshot (without extension)
Expected Results:
- ✅ With extension: ~20-30% of messages in DOM
- ✅ Without extension: 100% of messages in DOM
- ✅ Memory difference: 50-70% reduction
- ✅ Snapshot shows fewer Detached DOM nodes with extension
Measurement:
// In console (with extension active)
const totalMessages = document.querySelectorAll('article[data-testid^="conversation-turn-"]').length;
const placeholders = document.querySelectorAll('.trimwise-placeholder').length;
const inDOM = totalMessages;
const virtualized = placeholders;
console.log({
total: totalMessages + placeholders,
inDOM: inDOM,
virtualized: virtualized,
percentInDOM: (inDOM / (inDOM + virtualized) * 100).toFixed(1) + '%'
});Objective: Verify no unnecessary work during idle
Steps:
- Open ChatGPT with long conversation
- Open DevTools → Performance tab
- Click "Record"
- Let page sit idle for 30 seconds
- Stop recording
- Analyze timeline
Expected Results:
- ✅ No repeated 3-second spikes (old setInterval pattern)
- ✅ Minimal scripting activity during idle
- ✅ No "Trimwise" functions in flame graph
- ✅ CPU usage near 0%
Failure Modes:
- ❌ Regular spikes every 3 seconds → setInterval still running (check line 83)
- ❌ Constant activity → MutationObserver not debouncing
- ❌ High CPU → Infinite loop or observer thrashing
Objective: Verify instant response vs 3-second polling delay
Steps:
- Open ChatGPT conversation
- Open DevTools → Console
- Send a new message
- Watch console for "Trimwise" logs
- Note timestamp of message send vs update
Expected Results:
- ✅ Update happens within 100ms of message appearing
- ✅ Console shows MutationObserver triggered
- ✅ No 3-second wait
- ✅ Virtualization applies to new message if needed
Comparison:
- v1.1: 0-3 second random delay (depends on timer phase)
- v2.0: <100ms consistently (MutationObserver + debounce)
Objective: Verify settings saved and applied correctly
Steps:
- Right-click extension icon → Options
- Change batch size to 50
- Click Save
- Note the alert message
- Reload ChatGPT tab
- Count visible messages
Expected Results:
- ✅ Alert shows: "Settings saved. Please reload..."
- ✅ After reload, 50 messages visible (not default 20)
- ✅ "Show more" increments by 50
- ✅ Settings persist after browser restart
Failure Modes:
- ❌ Still shows 20 → Check
chrome.storage.sync.get()callback - ❌ Settings reset → Check storage permissions in manifest
- ❌ Wrong count → Check
BATCH_SIZEparseInt
Objective: Verify robustness in unusual scenarios
Expected: No "Show more" button, all messages visible
Expected: Smooth, no crashes, messages restore correctly
Expected: Heights still accurate, no jumping
Expected: Virtual scrolling adapts, no issues
Expected: No interference, message updates smoothly
Expected: Syntax highlighting preserved after restore
Expected: Images reload correctly, no broken src
Expected: Height measured correctly, restore smooth
Run in DevTools Console on ChatGPT page:
/**
* Trimwise v2.0 Automated Test Suite
* Run in Chrome DevTools Console on chatgpt.com
*/
(async function testTrimwise() {
console.log('🧪 Trimwise Test Suite v2.0\n');
const tests = [];
let passed = 0;
let failed = 0;
// Test 1: Extension loaded
tests.push({
name: 'Extension loaded',
fn: () => {
const style = document.getElementById('trimwise-styles');
return style !== null;
}
});
// Test 2: Articles found
tests.push({
name: 'Conversation articles detected',
fn: () => {
const articles = document.querySelectorAll('article[data-testid^="conversation-turn-"]');
console.log(` Found ${articles.length} articles`);
return articles.length > 0;
}
});
// Test 3: Show more button exists
tests.push({
name: 'Show more button present',
fn: () => {
const button = document.querySelector('.trimwise-button');
return button !== null;
}
});
// Test 4: Some messages hidden
tests.push({
name: 'Visibility rules applied',
fn: () => {
const hidden = document.querySelectorAll('.trimwise-hidden');
console.log(` ${hidden.length} messages hidden`);
return hidden.length > 0;
}
});
// Test 5: Virtualization active
tests.push({
name: 'Virtual scrolling enabled',
fn: () => {
const placeholders = document.querySelectorAll('.trimwise-placeholder');
console.log(` ${placeholders.length} messages virtualized`);
return true; // May be 0 if all in viewport
}
});
// Test 6: Placeholders have height
tests.push({
name: 'Placeholders preserve height',
fn: () => {
const placeholders = document.querySelectorAll('.trimwise-placeholder');
if (placeholders.length === 0) return true; // Skip if none
for (const p of placeholders) {
const height = parseInt(p.style.height);
if (isNaN(height) || height === 0) {
console.log(` ❌ Placeholder has invalid height: ${p.style.height}`);
return false;
}
}
return true;
}
});
// Test 7: No duplicate indices
tests.push({
name: 'No duplicate message indices',
fn: () => {
const articles = document.querySelectorAll('article[data-trimwise-index]');
const indices = Array.from(articles).map(a => a.dataset.trimwiseIndex);
const unique = new Set(indices);
return indices.length === unique.size;
}
});
// Test 8: Button has click handler
tests.push({
name: 'Button is interactive',
fn: () => {
const button = document.querySelector('.trimwise-button');
return button && typeof button.onclick === 'function';
}
});
// Run all tests
for (const test of tests) {
try {
const result = test.fn();
if (result) {
console.log(`✅ ${test.name}`);
passed++;
} else {
console.log(`❌ ${test.name}`);
failed++;
}
} catch (err) {
console.log(`❌ ${test.name} - Error: ${err.message}`);
failed++;
}
}
// Summary
console.log('\n📊 Test Results:');
console.log(` Passed: ${passed}`);
console.log(` Failed: ${failed}`);
console.log(` Total: ${tests.length}`);
if (failed === 0) {
console.log('\n✨ All tests passed! Extension working correctly.');
} else {
console.log('\n⚠️ Some tests failed. Check implementation.');
}
// Performance stats
console.log('\n📈 Performance Stats:');
const totalArticles = document.querySelectorAll('article[data-testid^="conversation-turn-"]').length;
const placeholders = document.querySelectorAll('.trimwise-placeholder').length;
const total = totalArticles + placeholders;
const inDOM = totalArticles;
const percentInDOM = total > 0 ? (inDOM / total * 100).toFixed(1) : 100;
console.log(` Total messages: ${total}`);
console.log(` In DOM: ${inDOM} (${percentInDOM}%)`);
console.log(` Virtualized: ${placeholders} (${(100 - percentInDOM).toFixed(1)}%)`);
console.log(` Memory reduction: ~${(100 - percentInDOM).toFixed(1)}%`);
})();Test on conversation with 500 messages:
| Implementation | DOM Nodes | Memory Usage | % Reduction |
|---|---|---|---|
| No Extension | ~50,000 | ~800 MB | 0% |
| v1.1 (hide) | ~50,000 | ~800 MB | 0% |
| v2.0 (virtual) | ~10,000 | ~200 MB | 75% |
Measure over 60 seconds idle:
| Implementation | Avg CPU | Peak CPU | Script Time |
|---|---|---|---|
| No Extension | 2% | 5% | 0ms |
| v1.1 (poll) | 8% | 15% | 500ms |
| v2.0 (event) | 2% | 3% | 25ms |
Fix: ChatGPT changed selectors
- Update
startObserving()to find new container - Try:
document.querySelector('[role="main"]')ordocument.querySelector('.conversation-container')
Fix: Check buffer thresholds
- Increase
rootMargininmessageObserver - Ensure messages actually leave viewport
- Check console for errors in
virtualizeMessage()
Fix: Height measurement inaccurate
- Check if
getBoundingClientRect()called BEFORE removal - Verify CSS transitions aren't affecting height
- Add small delay before measurement if needed
Fix: Observer not triggering
- Check
placeholderObserverrootMargin - Verify placeholders have
data-trimwise-placeholderattribute - Check if
_trimwiseCachedArticleproperty set
Fix: Messages not being removed
- Verify placeholders in DOM, not articles
- Check
virtualizedMessagesMap size - Ensure
replaceChild()actually removes old node
If you find issues, include:
- Browser: Chrome version
- Extension: Trimwise version
- Test: Which test failed
- Console: Any error messages
- Conversation: Message count
- Settings: Batch size
- Steps: How to reproduce
Submit to: https://github.com/garanovich/Trimwise/issues
Happy Testing! 🚀