Greasy Fork is available in English.
A user script to optimize ChatGPT's DOM structure, effectively preventing lag during long conversations.
// ==UserScript==
// @name ChatGPT Web Optimizer
// @name:zh-TW ChatGPT Web 優化器
// @name:zh-CN ChatGPT Web 优化器
// @name:ja ChatGPT Web オプティマイザ
// @namespace https://github.com/April-15/tampermonkey-scripts/blob/main/ChatGPT_Web_Optimizer.js
// @version 0.1.0
// @description A user script to optimize ChatGPT's DOM structure, effectively preventing lag during long conversations.
// @description:zh-TW 優化 ChatGPT 網頁效能,自動隔離過往聊天對話,解決長對話造成的網頁卡頓問題。
// @description:zh-CN 优化 ChatGPT 网页性能,自动隔离过往聊天对话,解决长对话造成的网页卡顿问题。
// @description:ja ChatGPTの長文チャットによるラグを防ぐため、過去のチャットを自動的に分離してウェブページのパフォーマンスを最適化するスクリプトです。
// @author April 15th
// @match https://chatgpt.com/*
// @icon https://chatgpt.com/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ============================================
// i18n / 多语言设置
// ============================================
const i18n = {
en: {
title: "ChatGPT Web Optimizer",
enable: "Enable Optimization",
bufferDistance: "Buffer Margin px",
language: "Language/语言",
save: "Save & Reload",
info: "Info: Scrolled out nodes are hidden via content-visibility to eliminate reflow lag without breaking React. Save & Reload to apply.",
optimizedCount: "Hidden Nodes: ",
panelToggle: "DOM Optimizer",
hideBtn: "Hide Floating Button (Open via Menu)"
},
zh_CN: {
title: "ChatGPT Web 优化",
enable: "启用优化",
bufferDistance: "缓冲距离 px",
language: "语言/Language",
save: "保存并刷新",
info: "当对话节点滚出视口+设定缓冲距离后,自动通过 CSS 隔离计算开销,彻底消除打字卡顿。更改设置后请保存刷新生效。",
optimizedCount: "当前隐藏节点: ",
panelToggle: "DOM优化",
hideBtn: "隐藏悬浮按钮 (仅从菜单打开)"
},
zh_TW: {
title: "ChatGPT Web 優化器",
enable: "啟用優化",
bufferDistance: "緩衝距離 px",
language: "語言/Language",
save: "保存並刷新",
info: "當對話節點滾出視口+設定緩衝距離後,自動通過 CSS 隔離計算開銷,徹底消除打字卡頓。更改設定後請保存刷新生效。",
optimizedCount: "當前隱藏節點: ",
panelToggle: "DOM優化",
hideBtn: "隱藏懸浮按鈕 (僅從菜單打開)"
},
ja: {
title: "ChatGPT Web オプティマイザ",
enable: "最適化を有効化",
bufferDistance: "バッファマージン px",
language: "言語/Language",
save: "保存して再読み込み",
info: "情報: ビューポート外のノードは content-visibility によって非表示になり、React を壊さずにリフローラグを排除します。適用するには保存して再読み込みしてください。",
optimizedCount: "非表示ノード数: ",
panelToggle: "DOM最適化",
hideBtn: "フローティングボタンを非表示 (メニューから開く)"
}
};
// ============================================
// 配置与状态保存
// ============================================
let settings = {
enabled: GM_getValue('enabled', true),
bufferDistance: parseInt(GM_getValue('bufferDistance', 2500)),
lang: GM_getValue('lang', 'en'),
hideBtn: GM_getValue('hideBtn', false)
};
const t = i18n[settings.lang] || i18n['en'];
// ============================================
// 核心逻辑: CSS-based Virtual DOM Manager
// ============================================
const hiddenSections = new Set();
let statsElement = null;
function updateOptimizedCount() {
if (statsElement) {
statsElement.textContent = t.optimizedCount + hiddenSections.size;
}
}
const domObserver = new IntersectionObserver((entries) => {
if (!settings.enabled) return;
entries.forEach(entry => {
const section = entry.target;
const turnId = section.getAttribute('data-turn-id') || section.getAttribute('data-testid');
if (!turnId) return;
if (!entry.isIntersecting) {
// Out of view -> Hide internal nodes
if (!hiddenSections.has(turnId)) {
const rect = section.getBoundingClientRect();
if (rect.height > 10) {
// Lock exact height
section.style.boxSizing = 'border-box';
section.style.height = rect.height + 'px';
// Strict CSS containment stops reflow propagation
section.style.contain = 'strict';
section.style.contentVisibility = 'hidden';
// Apply display: none to children as a fallback for absolute isolation
Array.from(section.children).forEach(child => {
child.dataset.optiOrigDisplay = child.style.display || '';
child.style.display = 'none';
});
hiddenSections.add(turnId);
updateOptimizedCount();
}
}
} else {
// In view -> Restore
if (hiddenSections.has(turnId)) {
// Remove locks
section.style.height = '';
section.style.boxSizing = '';
section.style.contain = '';
section.style.contentVisibility = '';
// Recover children
Array.from(section.children).forEach(child => {
if (child.hasAttribute('data-opti-orig-display')) {
child.style.display = child.dataset.optiOrigDisplay;
child.removeAttribute('data-opti-orig-display');
}
});
hiddenSections.delete(turnId);
updateOptimizedCount();
}
}
});
}, {
// Expand root margin so elements render well before they reach screen
rootMargin: `${settings.bufferDistance}px 0px`
});
function observeNewNodes() {
const sections = document.querySelectorAll('section[data-testid^="conversation-turn-"]');
sections.forEach(sec => domObserver.observe(sec));
}
// Used to detect dynamically appended React messages
const mutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Node.ELEMENT_NODE
if (node.tagName && node.tagName.toLowerCase() === 'section' && node.matches('[data-testid^="conversation-turn-"]')) {
domObserver.observe(node);
} else if (node.querySelectorAll) {
const sections = node.querySelectorAll('section[data-testid^="conversation-turn-"]');
if (sections.length > 0) {
sections.forEach(sec => domObserver.observe(sec));
}
}
}
});
});
});
// ============================================
// 初始化 & UI 组件挂载 (安全隔离 Shadow DOM)
// ============================================
function renderUIPanel() {
const host = document.createElement('div');
host.id = 'opti-cgpt-host';
host.style.position = 'fixed';
host.style.bottom = '20px';
host.style.right = '20px';
host.style.zIndex = '2147483647'; // Maximum z-index
// Attach Shadow DOM so ChatGPTs CSS does not hide our settings panel
const shadow = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
* { box-sizing: border-box; }
#opti-panel {
width: 260px;
background-color: #202123;
color: #ececf1;
border: 1px solid #4d4d4f;
padding: 16px;
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
box-shadow: 0 4px 20px rgba(0,0,0,0.6);
display: none;
flex-direction: column;
gap: 12px;
position: absolute;
bottom: 45px;
right: 0;
}
#opti-panel.show {
display: flex;
}
.opti-header {
font-weight: 600;
font-size: 15px;
cursor: default;
user-select: none;
padding-bottom: 6px;
border-bottom: 1px solid #4d4d4f;
}
.opti-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
}
select, input {
background: #343541;
color: white;
border: 1px solid #565869;
border-radius: 6px;
padding: 6px;
outline: none;
}
select:focus, input:focus { border-color: #10a37f; }
input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; }
button.opti-save-btn {
background-color: #10a37f;
color: white;
border: none;
padding: 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin-top: 5px;
transition: background 0.2s;
}
button.opti-save-btn:hover { background-color: #0b8c6c; }
.opti-info {
font-size: 11px;
color: #8e8ea0;
line-height: המח 1.4;
}
#opti-stats {
font-size: 12px;
color: #10a37f;
font-weight: 600;
}
.opti-toggle-btn {
background: #343541;
color: #ececf1;
border: 1px solid #565869;
border-radius: 8px;
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
transition: opacity 0.2s;
opacity: 0.6;
}
.opti-toggle-btn:hover { opacity: 1; }
`;
shadow.appendChild(style);
// Toggle Button
const toggleBtn = document.createElement('button');
toggleBtn.className = 'opti-toggle-btn';
toggleBtn.textContent = '⚙️ ' + t.panelToggle;
if (settings.hideBtn) {
toggleBtn.style.display = 'none';
}
shadow.appendChild(toggleBtn);
// Panel Container
const panel = document.createElement('div');
panel.id = 'opti-panel';
panel.innerHTML = `
<div class="opti-header">🎯 ${t.title}</div>
<div class="opti-row">
<label for="opti-enable">${t.enable}</label>
<input type="checkbox" id="opti-enable" ${settings.enabled ? 'checked' : ''}>
</div>
<div class="opti-row">
<label for="opti-lang">${t.language}</label>
<select id="opti-lang">
<option value="zh_CN" ${settings.lang === 'zh_CN' ? 'selected' : ''}>简体中文 (zh-CN)</option>
<option value="zh_TW" ${settings.lang === 'zh_TW' ? 'selected' : ''}>繁體中文 (zh-TW)</option>
<option value="en" ${settings.lang === 'en' ? 'selected' : ''}>English (en)</option>
<option value="ja" ${settings.lang === 'ja' ? 'selected' : ''}>日本語 (ja)</option>
</select>
</div>
<div class="opti-row">
<label for="opti-buffer">${t.bufferDistance}</label>
<input type="number" id="opti-buffer" style="width: 70px;" step="500" min="500" value="${settings.bufferDistance}">
</div>
<div class="opti-row">
<label for="opti-hideBtn">${t.hideBtn}</label>
<input type="checkbox" id="opti-hideBtn" ${settings.hideBtn ? 'checked' : ''}>
</div>
<div class="opti-info">${t.info}</div>
<div id="opti-stats">${t.optimizedCount}0</div>
<button class="opti-save-btn" id="opti-save">${t.save}</button>
`;
shadow.appendChild(panel);
document.body.appendChild(host);
statsElement = shadow.getElementById('opti-stats');
// Toggle Behavior
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
panel.classList.toggle('show');
});
// Click outside to close
document.addEventListener('click', (e) => {
if (e.composedPath().includes(host)) return;
panel.classList.remove('show');
});
// Save Function
shadow.getElementById('opti-save').addEventListener('click', () => {
GM_setValue('enabled', shadow.getElementById('opti-enable').checked);
GM_setValue('lang', shadow.getElementById('opti-lang').value);
GM_setValue('bufferDistance', shadow.getElementById('opti-buffer').value);
GM_setValue('hideBtn', shadow.getElementById('opti-hideBtn').checked);
window.location.reload();
});
GM_registerMenuCommand(t.panelToggle, () => {
panel.classList.add('show');
});
}
// ============================================
// 防崩溃启动程序
// ============================================
function bootstrap() {
if (!document.body) {
// Wait for body to be available
setTimeout(bootstrap, 200);
return;
}
try {
renderUIPanel();
if (settings.enabled) {
// Initialize observer
mutationObserver.observe(document.body, { childList: true, subtree: true });
// Grab any already loaded elements
observeNewNodes();
}
} catch (e) {
console.error('[OptiCGPTWeb] Failed to initialize:', e);
}
}
bootstrap();
})();