Chatgpt large chat lag fix A developer created a userscript called "ChatGPT Large Chat Performance" that fixes lag in very large ChatGPT conversations. The script uses CSS containment and detachable turbo scrolling to make long chats less sluggish by hiding thread content during rapid scrolling and restoring it when scrolling stops. | // ==UserScript== | | | // @name ChatGPT Large Chat Performance | | | // @namespace local.chatgpt.performance | | | // @version 0.4.0 | | | // @description Makes very large ChatGPT conversations less sluggish with CSS containment and detachable turbo scrolling. | | | // @match https://chatgpt.com/ | | | // @match https://chat.openai.com/ | | | // @run-at document-idle | | | // @grant none | | | // ==/UserScript== | | | function { | | | 'use strict'; | | | const STYLE ID = 'tm-chatgpt-large-chat-performance-style'; | | | const HEIGHT VAR = '--tm-cgpt-turn-height'; | | | const TURN SELECTOR = ' data-turn-id-container '; | | | const SCROLL IDLE MS = 180; | | | let enabled = true; | | | let root = null; | | | let ro = null; | | | let mo = null; | | | let scanTimer = 0; | | | let scrollIdleTimer = 0; | | | let parked = false; | | | let parkedNodes = ; | | | let placeholder = null; | | | const seen = new WeakSet ; | | | function addStyles { | | | if document.getElementById STYLE ID return; | | | const style = document.createElement 'style' ; | | | style.id = STYLE ID; | | | style.textContent = | | | ${TURN SELECTOR} { | | | content-visibility: auto important; | | | contain: layout style paint important; | | | contain-intrinsic-size: auto var ${HEIGHT VAR}, 760px important; | | | } | | | .markdown, | | | .text-message, | | | pre, | | | code { | | | overflow-wrap: anywhere important; | | | } | | | video, | | | canvas, | | | iframe { | | | content-visibility: auto important; | | | contain: layout paint important; | | | } | | | tm-cgpt-thread-placeholder { | | | display: block important; | | | contain: strict important; | | | visibility: hidden important; | | | pointer-events: none important; | | | } | | | ; | | | document.documentElement.appendChild style ; | | | } | | | function findThreadScrollRoot { | | | const thread = document.querySelector ' thread' ; | | | for let el = thread?.parentElement; el; el = el.parentElement { | | | const style = getComputedStyle el ; | | | if / auto|scroll|overlay /.test style.overflowY && el.scrollHeight el.clientHeight + 200 { | | | return el; | | | } | | | } | | | return document.scrollingElement || document.documentElement; | | | } | | | function rememberHeight entry { | | | const height = Math.ceil entry.borderBoxSize?. 0 ?.blockSize || entry.contentRect.height ; | | | if height 40 entry.target.style.setProperty HEIGHT VAR, ${height}px ; | | | } | | | function observeTurn turn { | | | if seen.has turn return; | | | seen.add turn ; | | | ro.observe turn ; | | | } | | | function scanTurns { | | | if enabled || ro return; | | | document.querySelectorAll TURN SELECTOR .forEach observeTurn ; | | | } | | | function scheduleScan { | | | clearTimeout scanTimer ; | | | scanTimer = window.setTimeout scanTurns, 150 ; | | | } | | | function getThread { | | | return document.querySelector ' thread' ; | | | } | | | function parkThread { | | | if parked return; | | | const thread = getThread ; | | | if thread || thread.childNodes.length return; | | | const height = Math.max thread.scrollHeight, thread.getBoundingClientRect .height, 1000 ; | | | placeholder = document.createElement 'div' ; | | | placeholder.id = 'tm-cgpt-thread-placeholder'; | | | placeholder.style.minHeight = ${Math.ceil height }px ; | | | parkedNodes = ...thread.childNodes ; | | | thread.replaceChildren placeholder ; | | | thread.style.minHeight = ${Math.ceil height }px ; | | | parked = true; | | | } | | | function restoreThread { | | | if parked return; | | | const thread = getThread ; | | | if thread { | | | thread.replaceChildren ...parkedNodes ; | | | thread.style.minHeight = ''; | | | } | | | parkedNodes = ; | | | placeholder = null; | | | parked = false; | | | scheduleScan ; | | | } | | | function onScroll { | | | parkThread ; | | | clearTimeout scrollIdleTimer ; | | | scrollIdleTimer = window.setTimeout restoreThread, SCROLL IDLE MS ; | | | } | | | function start { | | | stop false ; | | | enabled = true; | | | addStyles ; | | | root = findThreadScrollRoot ; | | | ro = new ResizeObserver entries = entries.forEach rememberHeight ; | | | scanTurns ; | | | mo = new MutationObserver scheduleScan ; | | | mo.observe document.body, { childList: true, subtree: true } ; | | | root?.addEventListener 'scroll', onScroll, { passive: true } ; | | | } | | | function stop removeStyles = true { | | | root?.removeEventListener 'scroll', onScroll ; | | | ro?.disconnect ; | | | mo?.disconnect ; | | | root = null; | | | ro = null; | | | mo = null; | | | clearTimeout scanTimer ; | | | clearTimeout scrollIdleTimer ; | | | restoreThread ; | | | if removeStyles document.getElementById STYLE ID ?.remove ; | | | } | | | function toggle { | | | enabled = enabled; | | | if enabled start ; | | | else stop ; | | | console.info ChatGPT Large Chat Performance ${enabled ? 'enabled' : 'disabled'} ; | | | } | | | window.addEventListener 'keydown', event = { | | | if event.altKey && event.code === 'KeyV' toggle ; | | | } ; | | | start ; | | | } ; |