Greasy Fork is available in English.
Preload Bilibili video comments on page load so the comment section is ready before you scroll to it.
当前为
// ==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();
}
})();