ChatGPT LagFix Lite

Reduces ChatGPT UI lag by disabling costly visuals, collapsing old turns, lazy-loading media, and trimming sidebar rendering.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         ChatGPT LagFix Lite
// @namespace    local.chatgpt.lagfix
// @version      1.0.0
// @license      MIT
// @description  Reduces ChatGPT UI lag by disabling costly visuals, collapsing old turns, lazy-loading media, and trimming sidebar rendering.
// @author       rexxx
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  const STORAGE_KEY = "chatgpt_lagfix_lite_settings_v1";

  const DEFAULTS = {
    speedMode: true,
    collapseOldTurns: true,
    keepLatestTurns: 12,
    trimSidebar: true,
    keepSidebarItems: 30,
    hideSidebar: false,
    lazyMedia: true,
    deChrome: true,
    showPanel: true
  };

  let settings = loadSettings();
  let scanQueued = false;
  let observerStarted = false;

  function loadSettings() {
    try {
      return { ...DEFAULTS, ...JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}") };
    } catch {
      return { ...DEFAULTS };
    }
  }

  function saveSettings() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
  }

  function idle(fn) {
    if ("requestIdleCallback" in window) {
      requestIdleCallback(fn, { timeout: 800 });
    } else {
      setTimeout(fn, 80);
    }
  }

  function queueScan() {
    if (scanQueued) return;
    scanQueued = true;

    setTimeout(() => {
      scanQueued = false;
      idle(scan);
    }, 120);
  }

  function injectStyle() {
    if (document.getElementById("cglf-style")) return;

    const style = document.createElement("style");
    style.id = "cglf-style";
    style.textContent = `
      html.cglf-speed *,
      html.cglf-speed *::before,
      html.cglf-speed *::after {
        animation-duration: 1ms !important;
        animation-delay: 0ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 1ms !important;
        transition-delay: 0ms !important;
        scroll-behavior: auto !important;
      }

      html.cglf-speed [class*="animate-"],
      html.cglf-speed [style*="animation"] {
        animation: none !important;
      }

      html.cglf-speed [class*="backdrop"],
      html.cglf-speed [style*="backdrop-filter"],
      html.cglf-speed [style*="-webkit-backdrop-filter"] {
        backdrop-filter: none !important;
        -webkit-backdrop-filter: none !important;
      }

      html.cglf-speed [class*="shadow"],
      html.cglf-speed [style*="box-shadow"] {
        box-shadow: none !important;
      }

      html.cglf-speed main article,
      html.cglf-speed main [data-message-author-role] {
        content-visibility: auto;
        contain-intrinsic-size: 1px 180px;
      }

      html.cglf-collapse [data-cglf-old-turn="1"]:not([data-cglf-expanded="1"]) > :not(.cglf-placeholder) {
        display: none !important;
      }

      html.cglf-collapse [data-cglf-old-turn="1"] {
        contain: layout paint style !important;
        margin-block: 4px !important;
      }

      .cglf-placeholder {
        width: 100%;
        display: block;
        text-align: left;
        cursor: pointer;
        border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
        border-radius: 10px;
        padding: 7px 10px;
        margin: 4px 0;
        font: 12px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        opacity: 0.72;
        background: color-mix(in srgb, Canvas 85%, currentColor 5%);
        color: inherit;
      }

      .cglf-placeholder:hover {
        opacity: 1;
      }

      html.cglf-trim-sidebar [data-cglf-sidebar-old="1"] {
        display: none !important;
      }

      html.cglf-hide-sidebar aside,
      html.cglf-hide-sidebar nav[aria-label*="Chat" i],
      html.cglf-hide-sidebar [data-testid*="sidebar" i] {
        display: none !important;
      }

      html.cglf-dechrome [aria-label*="Share" i],
      html.cglf-dechrome [data-testid*="share" i],
      html.cglf-dechrome [aria-label*="Copy link" i],
      html.cglf-dechrome [data-testid*="announcement" i],
      html.cglf-dechrome [class*="announcement" i],
      html.cglf-dechrome [class*="upsell" i],
      html.cglf-dechrome [data-testid*="upsell" i] {
        display: none !important;
      }

      #cglf-panel {
        position: fixed;
        left: 10px;
        bottom: 10px;
        z-index: 2147483647;
        font: 12px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        color: CanvasText;
      }

      #cglf-panel.cglf-hidden {
        display: none !important;
      }

      #cglf-panel button {
        font: inherit;
      }

      #cglf-panel-toggle {
        border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
        border-radius: 999px;
        padding: 7px 10px;
        cursor: pointer;
        background: Canvas;
        color: CanvasText;
        opacity: 0.82;
      }

      #cglf-panel-toggle:hover {
        opacity: 1;
      }

      #cglf-panel-body {
        display: none;
        min-width: 210px;
        margin-top: 6px;
        padding: 8px;
        border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
        border-radius: 12px;
        background: Canvas;
        box-shadow: 0 8px 30px rgba(0,0,0,.18);
      }

      #cglf-panel.cglf-open #cglf-panel-body {
        display: block;
      }

      #cglf-panel-body button {
        width: 100%;
        display: block;
        margin: 5px 0;
        padding: 6px 8px;
        border-radius: 8px;
        border: 1px solid color-mix(in srgb, CanvasText 15%, transparent);
        background: color-mix(in srgb, Canvas 90%, CanvasText 5%);
        color: CanvasText;
        cursor: pointer;
        text-align: left;
      }

      #cglf-panel-body button:hover {
        background: color-mix(in srgb, Canvas 82%, CanvasText 9%);
      }

      #cglf-panel-body .cglf-note {
        opacity: .66;
        margin: 6px 2px 2px;
      }
    `;

    const root = document.head || document.documentElement;
    if (root) root.appendChild(style);
  }

  function applyRootClasses() {
    const root = document.documentElement;
    if (!root) return;

    root.classList.toggle("cglf-speed", !!settings.speedMode);
    root.classList.toggle("cglf-collapse", !!settings.collapseOldTurns);
    root.classList.toggle("cglf-trim-sidebar", !!settings.trimSidebar);
    root.classList.toggle("cglf-hide-sidebar", !!settings.hideSidebar);
    root.classList.toggle("cglf-dechrome", !!settings.deChrome);
  }

  function getConversationTurns() {
    const articleTurns = [
      ...document.querySelectorAll(
        'main article[data-testid*="conversation-turn" i], main article'
      )
    ].filter(el => el.querySelector('[data-message-author-role]'));

    if (articleTurns.length) return uniqueElements(articleTurns);

    const roleNodes = [
      ...document.querySelectorAll('main [data-message-author-role]')
    ];

    return uniqueElements(
      roleNodes.map(node =>
        node.closest('article, [data-testid*="conversation-turn" i], .group, .relative') || node
      )
    );
  }

  function uniqueElements(elements) {
    const seen = new Set();
    const out = [];

    for (const el of elements) {
      if (!el || seen.has(el)) continue;
      seen.add(el);
      out.push(el);
    }

    return out;
  }

  function summarizeTurn(turn) {
    const roleNode = turn.querySelector("[data-message-author-role]");
    const role = roleNode?.getAttribute("data-message-author-role") || "turn";

    let text = roleNode?.textContent || turn.textContent || "";
    text = text
      .replace(/\s+/g, " ")
      .replace(/^LagFix: hidden older .*?click to expand\.?\s*/i, "")
      .trim();

    if (text.length > 120) text = text.slice(0, 120).trim() + "…";
    if (!text) text = "content hidden";

    return { role, text };
  }

  function ensurePlaceholder(turn, index, total) {
    let placeholder = turn.querySelector(":scope > .cglf-placeholder");

    if (!placeholder) {
      placeholder = document.createElement("button");
      placeholder.type = "button";
      placeholder.className = "cglf-placeholder";
      placeholder.addEventListener("click", event => {
        event.preventDefault();
        event.stopPropagation();
        turn.dataset.cglfExpanded =
          turn.dataset.cglfExpanded === "1" ? "0" : "1";
      });

      turn.insertBefore(placeholder, turn.firstChild);
    }

    if (!placeholder.dataset.cglfReady) {
      const { role, text } = summarizeTurn(turn);
      placeholder.textContent =
        `LagFix: hidden older ${role} message ${index + 1}/${total} — click to expand. ${text}`;
      placeholder.dataset.cglfReady = "1";
    }
  }

  function clearPlaceholder(turn) {
    const placeholder = turn.querySelector(":scope > .cglf-placeholder");
    if (placeholder) placeholder.remove();
  }

  function collapseOldTurns() {
    const turns = getConversationTurns();
    const keep = Math.max(2, Number(settings.keepLatestTurns) || DEFAULTS.keepLatestTurns);
    const cutoff = Math.max(0, turns.length - keep);

    turns.forEach((turn, index) => {
      const old = settings.collapseOldTurns && index < cutoff;

      if (old) {
        turn.dataset.cglfOldTurn = "1";
        ensurePlaceholder(turn, index, turns.length);
      } else {
        delete turn.dataset.cglfOldTurn;
        delete turn.dataset.cglfExpanded;
        clearPlaceholder(turn);
      }
    });
  }

  function trimSidebar() {
    const links = [
      ...document.querySelectorAll(
        'aside a[href^="/c/"], nav a[href^="/c/"], aside a[href*="/c/"], nav a[href*="/c/"]'
      )
    ];

    const rows = uniqueElements(
      links.map(link =>
        link.closest('li, [role="listitem"], [data-testid*="conversation" i], div') || link
      )
    );

    const keep = Math.max(5, Number(settings.keepSidebarItems) || DEFAULTS.keepSidebarItems);

    rows.forEach((row, index) => {
      if (settings.trimSidebar && index >= keep) {
        row.dataset.cglfSidebarOld = "1";
      } else {
        delete row.dataset.cglfSidebarOld;
      }
    });
  }

  function lazyMedia() {
    if (!settings.lazyMedia) return;

    document.querySelectorAll("img:not([data-cglf-lazy])").forEach(img => {
      img.loading = "lazy";
      img.decoding = "async";
      img.dataset.cglfLazy = "1";
    });

    document.querySelectorAll("iframe:not([data-cglf-lazy])").forEach(frame => {
      frame.loading = "lazy";
      frame.dataset.cglfLazy = "1";
    });

    document.querySelectorAll("video:not([data-cglf-lazy])").forEach(video => {
      video.preload = "none";
      video.dataset.cglfLazy = "1";
    });

    document
      .querySelectorAll('[data-cglf-old-turn="1"]:not([data-cglf-expanded="1"]) video')
      .forEach(video => {
        try {
          video.pause();
        } catch {}
      });
  }

  function ensurePanel() {
    if (!settings.showPanel) return;
    if (!document.body || document.getElementById("cglf-panel")) return;

    const panel = document.createElement("div");
    panel.id = "cglf-panel";
    panel.innerHTML = `
      <button id="cglf-panel-toggle" type="button" title="ChatGPT LagFix Lite">⚡ LagFix</button>
      <div id="cglf-panel-body">
        <button type="button" data-cglf-action="speed"></button>
        <button type="button" data-cglf-action="collapse"></button>
        <button type="button" data-cglf-action="sidebar-trim"></button>
        <button type="button" data-cglf-action="sidebar-hide"></button>
        <button type="button" data-cglf-action="dechrome"></button>
        <button type="button" data-cglf-action="keep"></button>
        <button type="button" data-cglf-action="reset">Reset LagFix settings</button>
        <div class="cglf-note">Hotkeys: Alt+Shift+L speed, Alt+Shift+O old turns, Alt+Shift+S sidebar.</div>
      </div>
    `;

    document.body.appendChild(panel);

    panel.querySelector("#cglf-panel-toggle").addEventListener("click", () => {
      panel.classList.toggle("cglf-open");
    });

    panel.addEventListener("click", event => {
      const button = event.target.closest("[data-cglf-action]");
      if (!button) return;

      const action = button.dataset.cglfAction;

      if (action === "speed") settings.speedMode = !settings.speedMode;
      if (action === "collapse") settings.collapseOldTurns = !settings.collapseOldTurns;
      if (action === "sidebar-trim") settings.trimSidebar = !settings.trimSidebar;
      if (action === "sidebar-hide") settings.hideSidebar = !settings.hideSidebar;
      if (action === "dechrome") settings.deChrome = !settings.deChrome;

      if (action === "keep") {
        const next = Number(prompt("Keep how many latest messages fully rendered?", String(settings.keepLatestTurns)));
        if (Number.isFinite(next) && next >= 2 && next <= 100) {
          settings.keepLatestTurns = Math.floor(next);
        }
      }

      if (action === "reset") {
        settings = { ...DEFAULTS };
      }

      saveSettings();
      applyRootClasses();
      updatePanel();
      queueScan();
    });

    updatePanel();
  }

  function updatePanel() {
    const panel = document.getElementById("cglf-panel");
    if (!panel) return;

    const label = (name, on) => `${on ? "✓" : "○"} ${name}`;

    const set = (action, text) => {
      const btn = panel.querySelector(`[data-cglf-action="${action}"]`);
      if (btn) btn.textContent = text;
    };

    set("speed", label("Speed mode", settings.speedMode));
    set("collapse", label("Collapse old messages", settings.collapseOldTurns));
    set("sidebar-trim", label("Trim sidebar history", settings.trimSidebar));
    set("sidebar-hide", label("Hide sidebar", settings.hideSidebar));
    set("dechrome", label("Hide extra chrome", settings.deChrome));
    set("keep", `Keep latest messages: ${settings.keepLatestTurns}`);
  }

  function bindHotkeys() {
    if (window.__cglfHotkeysBound) return;
    window.__cglfHotkeysBound = true;

    document.addEventListener("keydown", event => {
      if (!event.altKey || !event.shiftKey) return;

      const code = event.code;

      if (code === "KeyL") {
        settings.speedMode = !settings.speedMode;
      } else if (code === "KeyO") {
        settings.collapseOldTurns = !settings.collapseOldTurns;
      } else if (code === "KeyS") {
        settings.hideSidebar = !settings.hideSidebar;
      } else {
        return;
      }

      event.preventDefault();
      saveSettings();
      applyRootClasses();
      updatePanel();
      queueScan();
    });
  }

  function scan() {
    injectStyle();
    applyRootClasses();
    ensurePanel();
    bindHotkeys();
    collapseOldTurns();
    trimSidebar();
    lazyMedia();
  }

  function startObserver() {
    if (observerStarted || !document.documentElement) return;
    observerStarted = true;

    const observer = new MutationObserver(queueScan);
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });

    queueScan();
  }

  injectStyle();
  applyRootClasses();
  startObserver();

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      scan();
      startObserver();
    }, { once: true });
  } else {
    scan();
    startObserver();
  }
})();