ChatGPT LagFix Lite

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
  }
})();