Greasy Fork

Greasy Fork is available in English.

Bilibili Immediate Comments Preload

Preload Bilibili video comments on page load so the comment section is ready before you scroll to it.

当前为 2026-04-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bilibili Immediate Comments Preload
// @name:zh-TW   Bilibili 留言區提前預載
// @namespace    https://github.com/jellycat/bilibili-watchitlater-quickdigest
// @version      0.1.0
// @description  Preload Bilibili video comments on page load so the comment section is ready before you scroll to it.
// @description:zh-TW  在 Bilibili 影片頁一開啟時就提前載入留言區,避免每次都要先往下捲再等留言動態出現。
// @author       jellycat
// @match        https://www.bilibili.com/video/*
// @run-at       document-start
// @grant        none
// @noframes
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  const DEBUG_KEY = '__BILI_COMMENT_PRELOAD_USERSCRIPT__';
  const DISABLE_AUTOBOOT_KEY = '__BILI_COMMENT_PRELOAD_DISABLE_AUTOBOOT__';
  const NAV_EVENT = 'bili-comment-preload:urlchange';
  const NAV_HOOK_FLAG = '__biliCommentPreloadNavHookInstalled__';
  const HOST_ATTEMPTS = new WeakMap();
  const MAX_ATTEMPTS_PER_HOST = 3;
  const MIN_RETRY_INTERVAL_MS = 1_500;
  const WAIT_TIMEOUT_MS = 15_000;
  const POLL_INTERVAL_MS = 250;
  const RETRY_DELAYS_MS = [0, 400, 1_200, 3_000];

  let navigationToken = 0;

  function isVideoPage(url = globalThis.location?.href ?? '') {
    return /^https:\/\/www\.bilibili\.com\/video\/[^/?#]+/i.test(String(url));
  }

  function getCommentHost(root = document) {
    return root.querySelector('#commentapp > bili-comments');
  }

  function hasTruthyAttribute(element, name) {
    if (!element?.hasAttribute?.(name)) {
      return false;
    }
    const value = element.getAttribute(name);
    return value !== 'false';
  }

  function normalizeOptionalValue(value) {
    return value == null || value === '' ? undefined : value;
  }

  function splitDataParams(host) {
    const raw = host?.getAttribute?.('data-params') ?? '';
    const [type = '', oid = ''] = raw.split(',');
    return { type, oid };
  }

  function hasLoadedThreads(host) {
    const shadowRoot = host?.shadowRoot;
    return Boolean(shadowRoot?.querySelector('bili-comment-thread-renderer'));
  }

  function extractReloadOptions(host) {
    if (!host) {
      return null;
    }

    const { type, oid } = splitDataParams(host);
    if (!type || !oid) {
      return null;
    }

    const options = {
      oid,
      type,
      lazyLoad: false,
      spmPrefix: host.getAttribute('spm-prefix') ?? '',
      contentFeatures: {
        videoTime: !hasTruthyAttribute(host, 'disable-video-time')
      },
      fixedCommentBox: host.getAttribute('fixed-comment-box') !== 'false',
      disableUpActions: hasTruthyAttribute(host, 'disable-up-actions')
    };

    const optionalMap = {
      mode: 'mode',
      seekRpid: 'seek-id',
      maxViewLimit: 'max-view-limit',
      cmFromTrackId: 'cm-from-track-id'
    };

    for (const [optionKey, attributeName] of Object.entries(optionalMap)) {
      const value = normalizeOptionalValue(host.getAttribute(attributeName));
      if (value !== undefined) {
        options[optionKey] = value;
      }
    }

    return options;
  }

  function markPreloadAttempt(host) {
    const previous = HOST_ATTEMPTS.get(host) ?? { count: 0, lastAttemptAt: 0 };
    const next = {
      count: previous.count + 1,
      lastAttemptAt: Date.now()
    };
    HOST_ATTEMPTS.set(host, next);
    return next;
  }

  function canAttemptPreload(host) {
    const state = HOST_ATTEMPTS.get(host) ?? { count: 0, lastAttemptAt: 0 };
    return state.count < MAX_ATTEMPTS_PER_HOST && Date.now() - state.lastAttemptAt >= MIN_RETRY_INTERVAL_MS;
  }

  function preloadHost(host, reason = 'unknown') {
    if (!host || typeof host.reload !== 'function' || hasLoadedThreads(host) || !canAttemptPreload(host)) {
      return false;
    }

    const reloadOptions = extractReloadOptions(host);
    if (!reloadOptions) {
      return false;
    }

    host.removeAttribute('lazy-load');
    try {
      host.lazyLoad = false;
    } catch {
      // Ignore assignment failures on readonly descriptors.
    }

    markPreloadAttempt(host);

    try {
      host.reload(reloadOptions);
      host.dataset.biliCommentPreloaded = reason;
      return true;
    } catch (error) {
      console.warn('[bili-comment-preload] reload failed:', error);
      return false;
    }
  }

  function waitForUpgradedHost({ timeoutMs = WAIT_TIMEOUT_MS, token } = {}) {
    return new Promise((resolve) => {
      const deadline = Date.now() + timeoutMs;
      let pollTimer = null;
      let timeoutTimer = null;
      let observer = null;

      function cleanup() {
        if (pollTimer) {
          clearInterval(pollTimer);
        }
        if (timeoutTimer) {
          clearTimeout(timeoutTimer);
        }
        observer?.disconnect();
      }

      function finish(host) {
        cleanup();
        resolve(host ?? null);
      }

      function isStale() {
        return token != null && token !== navigationToken;
      }

      function probe() {
        if (isStale()) {
          finish(null);
          return;
        }

        const host = getCommentHost();
        if (host && typeof host.reload === 'function') {
          finish(host);
          return;
        }

        if (Date.now() >= deadline) {
          finish(host ?? null);
        }
      }

      if (document.documentElement) {
        observer = new MutationObserver(probe);
        observer.observe(document.documentElement, {
          subtree: true,
          childList: true,
          attributes: true
        });
      }

      pollTimer = setInterval(probe, POLL_INTERVAL_MS);
      timeoutTimer = setTimeout(() => finish(getCommentHost()), timeoutMs);

      if (globalThis.customElements?.whenDefined) {
        globalThis.customElements.whenDefined('bili-comments').then(probe).catch(() => {});
      }

      document.addEventListener('DOMContentLoaded', probe, { once: true });
      globalThis.addEventListener('load', probe, { once: true });
      probe();
    });
  }

  async function runPreloadPass(reason = 'manual', token = navigationToken) {
    if (!isVideoPage()) {
      return false;
    }

    const host = await waitForUpgradedHost({ token });
    if (!host || token !== navigationToken) {
      return false;
    }

    if (hasLoadedThreads(host)) {
      return false;
    }

    return preloadHost(host, reason);
  }

  function schedulePreload(reason = 'schedule') {
    if (!isVideoPage()) {
      return;
    }

    const token = ++navigationToken;
    RETRY_DELAYS_MS.forEach((delayMs, index) => {
      globalThis.setTimeout(() => {
        if (token !== navigationToken) {
          return;
        }
        runPreloadPass(`${reason}:${index}`, token).catch((error) => {
          console.warn('[bili-comment-preload] scheduled preload failed:', error);
        });
      }, delayMs);
    });
  }

  function dispatchUrlChange(kind) {
    globalThis.dispatchEvent(new CustomEvent(NAV_EVENT, {
      detail: {
        kind,
        href: globalThis.location?.href ?? ''
      }
    }));
  }

  function installNavigationHooks() {
    if (globalThis[NAV_HOOK_FLAG]) {
      return;
    }
    globalThis[NAV_HOOK_FLAG] = true;

    for (const methodName of ['pushState', 'replaceState']) {
      const original = history[methodName];
      if (typeof original !== 'function') {
        continue;
      }
      history[methodName] = function patchedHistoryMethod(...args) {
        const result = original.apply(this, args);
        queueMicrotask(() => dispatchUrlChange(methodName));
        return result;
      };
    }

    globalThis.addEventListener(NAV_EVENT, () => schedulePreload('urlchange'));
    globalThis.addEventListener('popstate', () => dispatchUrlChange('popstate'));
    globalThis.addEventListener('hashchange', () => dispatchUrlChange('hashchange'));
  }

  function boot() {
    installNavigationHooks();
    schedulePreload('boot');
    document.addEventListener('DOMContentLoaded', () => schedulePreload('domcontentloaded'), { once: true });
    globalThis.addEventListener('load', () => schedulePreload('load'), { once: true });
  }

  const api = {
    isVideoPage,
    getCommentHost,
    splitDataParams,
    hasLoadedThreads,
    extractReloadOptions,
    preloadHost,
    waitForUpgradedHost,
    runPreloadPass,
    schedulePreload,
    boot
  };

  globalThis[DEBUG_KEY] = api;

  if (!globalThis[DISABLE_AUTOBOOT_KEY]) {
    boot();
  }
})();