Greasy Fork

Ultimate Web Optimizer

全面的网页性能优化方案(含懒加载/预加载/预连接)

当前为 2025-06-09 提交的版本,查看 最新版本

// ==UserScript==
// @name         Ultimate Web Optimizer
// @namespace    https://greasyfork.org/zh-CN/users/1474228-moyu001
// @version      1.2.2
// @description  全面的网页性能优化方案(含懒加载/预加载/预连接)
// @author       moyu001
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_log
// @license      MIT
// @run-at       document-start
// ==/UserScript==

// 防debugger拦截(需尽早注入)
(function() {
    // 拦截 Function 构造器
    const origFunc = Function.prototype.constructor;
    Function.prototype.constructor = function(...args) {
        const code = args.join(',');
        if (code.includes('debugger')) {
            return origFunc.call(this, code.replace(/debugger;?/g, ''));
        }
        return origFunc.apply(this, args);
    };
    // 拦截 eval
    const origEval = window.eval;
    window.eval = function(code) {
        if (typeof code === 'string' && code.includes('debugger')) {
            code = code.replace(/debugger;?/g, '');
        }
        return origEval(code);
    };
})();

(function() {
    'use strict';

    // ========================
    // 配置中心
    // ========================
    // 如需持久化配置,可结合GM_setValue/GM_getValue实现用户自定义
    const config = {
        debug: false,  // 开启调试日志
        features: {
            lazyLoad: {  // 图片懒加载
                enabled: true,
                minSize: 100,     // 最小处理尺寸(px)
                rootMargin: '200px',  // 提前加载距离
                skipHidden: true  // 跳过隐藏图片
            },
            preconnect: {  // 智能预连接
                enabled: true,
                whitelist: [  // 中国大陆优化白名单
                    'fonts.gstatic.com',
                    'cdnjs.cloudflare.com',
                    'unpkg.com',
                    'ajax.googleapis.com',
                    'maxcdn.bootstrapcdn.com',
                    'code.jquery.com',
                    'kit.fontawesome.com',
                    'fonts.googleapis.cn',
                    'fonts.loli.net',
                    'cdn.jsdelivr.net',
                    'cdn.bootcdn.net',
                    'cdn.bootcss.com',
                    'libs.baidu.com',
                    'cdn.staticfile.org',
                    'lf3-cdn-tos.bytecdntp.com',
                    'unpkg.zhimg.com',
                    'npm.elemecdn.com',
                    'g.alicdn.com'
                ],
                maxConnections: 5  // 最大预连接数
            },
            preload: {  // 资源预加载
                enabled: true,
                types: ['css', 'js', 'woff2'],  // 预加载类型
                maxPreloads: 5  // 最大预加载数
            },
            layout: {  // 布局稳定性
                stableImages: true,
                stableIframes: true
            }
        }
    };

    // 全局黑名单(可扩展)
    const optimizerSiteBlacklist = [
        // 'example.com',
        // 可添加其它不希望优化的站点
    ];
    // 各模块独立黑名单(可扩展)
    const lazyLoadSiteBlacklist = [
        // 'example.com',
    ];
    const preconnectSiteBlacklist = [
        // 'example.com',
    ];
    const preloadSiteBlacklist = [
        // 'example.com',
    ];
    const layoutSiteBlacklist = [
        // 'example.com',
    ];

    // ========================
    // 核心优化模块
    // ========================

    // 防抖工具
    function debounce(fn, delay) {
        let timer = null;
        return function(...args) {
            clearTimeout(timer);
            timer = setTimeout(() => fn.apply(this, args), delay);
        };
    }

    // 简单LRU缓存实现
    class LRUSet {
        constructor(limit) {
            this.limit = limit;
            this.map = new Map();
        }
        has(key) {
            return this.map.has(key);
        }
        add(key) {
            if (this.map.has(key)) {
                this.map.delete(key);
            }
            this.map.set(key, true);
            if (this.map.size > this.limit) {
                // 删除最早的
                const firstKey = this.map.keys().next().value;
                this.map.delete(firstKey);
            }
        }
        get size() {
            return this.map.size;
        }
    }

    // 定期清理工具
    function scheduleCleanup(fn, interval = 10 * 60 * 1000) { // 默认10分钟
        setInterval(fn, interval);
    }

    // 图片懒加载系统(已移除视频相关优化)
    const initLazyLoad = () => {
        if (optimizerSiteBlacklist.some(domain => location.hostname.includes(domain)) ||
            lazyLoadSiteBlacklist.some(domain => location.hostname.includes(domain))) {
            if (config.debug) console.log('[懒加载] 当前站点已在全局或懒加载黑名单,跳过懒加载优化');
            return;
        }
        if (!config.features.lazyLoad.enabled) return;
        try {
            let lazyCount = 0;
            const isLazyCandidate = (el) => {
                // 跳过已实现懒加载的图片(如有data-src/data-srcset属性)
                if (el.hasAttribute && (el.hasAttribute('data-src') || el.hasAttribute('data-srcset'))) return false;
                if (el.loading === 'eager') return false;
                if (el.complete) return false;
                if (el.src && el.src.startsWith('data:')) return false;
                if (config.features.lazyLoad.skipHidden &&
                    window.getComputedStyle(el).display === 'none') return false;
                const rect = el.getBoundingClientRect();
                return rect.width > config.features.lazyLoad.minSize &&
                       rect.height > config.features.lazyLoad.minSize;
            };

            const processMedia = (el) => {
                if ('loading' in HTMLImageElement.prototype && el.tagName === 'IMG') {
                    el.loading = 'lazy';
                }
                if (!el.dataset.src && el.src) {
                    el.dataset.src = el.src;
                    el.removeAttribute('src');
                }
                if (el.srcset && !el.dataset.srcset) {
                    el.dataset.srcset = el.srcset;
                    el.removeAttribute('srcset');
                }
                lazyCount++;
                return el;
            };

            // 批量处理工具(自适应大页面)
            function batchProcess(arr, fn, batchSize, cb) {
                // batchSize自适应:图片极多时分批更小
                if (!batchSize) {
                    if (arr.length > 1000) batchSize = 8;
                    else if (arr.length > 500) batchSize = 16;
                    else if (arr.length > 200) batchSize = 32;
                    else batchSize = 64;
                }
                let i = 0;
                function nextBatch() {
                    for (let j = 0; j < batchSize && i < arr.length; j++, i++) {
                        fn(arr[i]);
                    }
                    if (i < arr.length) {
                        (window.requestIdleCallback || window.requestAnimationFrame)(nextBatch);
                    } else if (cb) {
                        cb();
                    }
                }
                nextBatch();
            }

            if ('IntersectionObserver' in window) {
                const observer = new IntersectionObserver((entries) => {
                    entries.forEach(entry => {
                        if (entry.isIntersecting) {
                            const el = entry.target;
                            if (el.dataset.src) el.src = el.dataset.src;
                            if (el.dataset.srcset) el.srcset = el.dataset.srcset;
                            observer.unobserve(el);
                        }
                    });
                }, {
                    rootMargin: config.features.lazyLoad.rootMargin,
                    threshold: 0.01
                });

                // 批量observe初始图片
                const imgs = Array.from(document.querySelectorAll('img')).filter(isLazyCandidate);
                batchProcess(imgs, el => observer.observe(processMedia(el)), undefined, () => {
                    if (config.debug) {
                        console.log(`[懒加载] 初始图片: ${imgs.length}`);
                    }
                });

                // 动态加载监听,合并多次变更
                let pendingMedia = [];
                let scheduled = false;
                function scheduleBatchObserve() {
                    if (scheduled) return;
                    scheduled = true;
                    (window.requestIdleCallback || window.requestAnimationFrame)(() => {
                        scheduled = false;
                        const arr = pendingMedia;
                        pendingMedia = [];
                        batchProcess(arr, el => observer.observe(processMedia(el)), undefined, () => {
                            if (config.debug && arr.length) {
                                console.log(`[懒加载] 新增图片: ${arr.length}`);
                            }
                        });
                    });
                }
                const observeNewMedia = debounce(mutations => {
                    mutations.forEach(mutation => {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeName === 'IMG' && isLazyCandidate(node)) {
                                pendingMedia.push(node);
                            }
                        });
                    });
                    if (pendingMedia.length) scheduleBatchObserve();
                }, 50);
                const mo = new MutationObserver(observeNewMedia);
                mo.observe(document.body, {
                    childList: true,
                    subtree: true
                });

                // 资源释放:监听移除节点,自动unobserve
                const unobserveRemoved = mutations => {
                    mutations.forEach(mutation => {
                        mutation.removedNodes.forEach(node => {
                            if (node.nodeName === 'IMG') {
                                observer.unobserve(node);
                            }
                        });
                    });
                };
                const mo2 = new MutationObserver(unobserveRemoved);
                mo2.observe(document.body, {
                    childList: true,
                    subtree: true
                });
            } else {
                // 兼容模式实现
                const checkVisible = throttle(() => {
                    document.querySelectorAll('img').forEach(img => {
                        if (isLazyCandidate(img) && !img.src) {
                            const rect = img.getBoundingClientRect();
                            if (rect.top < window.innerHeight +
                                parseInt(config.features.lazyLoad.rootMargin)) {
                                if (img.dataset.src) img.src = img.dataset.src;
                                if (img.dataset.srcset) img.srcset = img.dataset.srcset;
                            }
                        }
                    });
                }, 200);
                window.addEventListener('scroll', checkVisible);
                window.addEventListener('resize', checkVisible); // 新增resize监听
                checkVisible();
            }
        } catch (e) {
            if (config.debug) console.warn('[LazyLoad] 异常:', e);
        }
    };

    // 智能预连接系统
    const initSmartPreconnect = () => {
        if (optimizerSiteBlacklist.some(domain => location.hostname.includes(domain)) ||
            preconnectSiteBlacklist.some(domain => location.hostname.includes(domain))) {
            if (config.debug) console.log('[预连接] 当前站点已在全局或预连接黑名单,跳过预连接优化');
            return;
        }
        if (!config.features.preconnect.enabled) return;
        try {
            const processed = new LRUSet(config.features.preconnect.maxConnections);
            const whitelist = config.features.preconnect.whitelist;
            scheduleCleanup(() => {
                processed.map.clear && processed.map.clear();
                if (config.debug) console.log('[Preconnect] processed已清理');
            }, 10 * 60 * 1000);

            let preconnectCount = 0;
            const doPreconnect = (hostname) => {
                if (processed.has(hostname)) return;
                const link = document.createElement('link');
                link.rel = 'preconnect';
                link.href = `https://${hostname}`;
                document.head.appendChild(link);
                processed.add(hostname);
                preconnectCount++;
                if (config.debug) {
                    console.log('[Preconnect] 已连接:', hostname);
                }
            };

            const scanResources = () => {
                const resources = [
                    ...document.querySelectorAll('script[src], link[href], img[src]')
                ];
                resources.forEach(el => {
                    try {
                        const url = new URL(el.src || el.href);
                        const matched = whitelist.find(domain =>
                            url.hostname.endsWith(domain)
                        );
                        if (matched) {
                            doPreconnect(url.hostname);
                        }
                    } catch {}
                });
            };

            // MutationObserver回调防抖
            const debouncedScan = debounce(scanResources, 50);
            scanResources();
            setTimeout(scanResources, 2000);
            new MutationObserver(debouncedScan).observe(document.body, {
                childList: true,
                subtree: true
            });

            // 统计日志
            if (config.debug) {
                setTimeout(() => {
                    console.log(`[预连接] 本页已预连接: ${preconnectCount}`);
                }, 2500);
            }
        } catch (e) {
            if (config.debug) console.warn('[Preconnect] 异常:', e);
        }
    };

    // 资源预加载系统(修复字体预加载警告)
    const initResourcePreload = () => {
        if (optimizerSiteBlacklist.some(domain => location.hostname.includes(domain)) ||
            preloadSiteBlacklist.some(domain => location.hostname.includes(domain))) {
            if (config.debug) console.log('[预加载] 当前站点已在全局或预加载黑名单,跳过预加载优化');
            return;
        }
        if (!config.features.preload.enabled) return;
        try {
            const processed = new Set();
            const types = config.features.preload.types;
            const max = config.features.preload.maxPreloads;
            const cssCache = new Map();
            let preloadCount = 0;

            const shouldPreload = (url) => {
                const ext = url.split('.').pop().toLowerCase();
                return types.includes(ext);
            };

            const doPreload = (url, asType) => {
                if (processed.size >= max) return;
                if (processed.has(url)) return;
                const link = document.createElement('link');
                link.rel = 'preload';
                link.as = asType;
                link.href = url;
                if (asType === 'font') {
                    link.setAttribute('crossorigin', 'anonymous');
                    if (url.includes('.woff2')) {
                        link.type = 'font/woff2';
                    } else if (url.includes('.woff')) {
                        link.type = 'font/woff';
                    } else if (url.includes('.ttf')) {
                        link.type = 'font/ttf';
                    } else if (url.includes('.otf')) {
                        link.type = 'font/otf';
                    }
                } else if (asType === 'script') {
                    link.type = 'text/javascript';
                } else if (asType === 'style') {
                    link.type = 'text/css';
                }
                document.head.appendChild(link);
                processed.add(url);
                preloadCount++;
                if (config.debug) {
                    console.log('[Preload] 已预加载:', url);
                }
            };

            const processFont = (cssUrl, fontUrl) => {
                try {
                    const absoluteUrl = new URL(fontUrl, cssUrl).href;
                    if (fontUrl.startsWith('data:')) return;
                    if (shouldPreload(absoluteUrl)) {
                        doPreload(absoluteUrl, 'font');
                    }
                } catch (e) {
                    if (config.debug) {
                        console.warn('[Preload] 字体解析失败:', fontUrl, e);
                    }
                }
            };

            const scanResources = () => {
                document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
                    const cssUrl = link.href;
                    if (cssUrl && shouldPreload(cssUrl)) {
                        doPreload(cssUrl, 'style');
                        // fetch缓存
                        if (cssCache.has(cssUrl)) {
                            const text = cssCache.get(cssUrl);
                            const fontUrls = text.match(/url\(["']?([^")']+\.(woff2?|ttf|otf))["']?\)/gi) || [];
                            fontUrls.forEach(fullUrl => {
                                const cleanUrl = fullUrl.replace(/url\(["']?|["']?\)/g, '');
                                processFont(cssUrl, cleanUrl);
                            });
                        } else {
                            fetch(cssUrl)
                                .then(res => res.text())
                                .then(text => {
                                    cssCache.set(cssUrl, text);
                                    const fontUrls = text.match(/url\(["']?([^")']+\.(woff2?|ttf|otf))["']?\)/gi) || [];
                                    fontUrls.forEach(fullUrl => {
                                        const cleanUrl = fullUrl.replace(/url\(["']?|["']?\)/g, '');
                                        processFont(cssUrl, cleanUrl);
                                    });
                                })
                                .catch(e => {
                                    if (config.debug) {
                                        console.warn('[Preload] CSS获取失败:', cssUrl, e);
                                    }
                                });
                        }
                    }
                });
                document.querySelectorAll('script[src]').forEach(script => {
                    const src = script.src;
                    if (src && shouldPreload(src)) {
                        doPreload(src, 'script');
                    }
                });
            };

            // MutationObserver回调防抖
            const debouncedScan = debounce(scanResources, 50);
            scanResources();
            new MutationObserver(debouncedScan).observe(document.body, {
                childList: true,
                subtree: true
            });

            // 定期清理cssCache和processed,防止无限增长
            scheduleCleanup(() => {
                cssCache.clear();
                processed.clear();
                if (config.debug) console.log('[Preload] 缓存已清理');
            }, 10 * 60 * 1000); // 10分钟

            // 统计日志
            if (config.debug) {
                setTimeout(() => {
                    console.log(`[预加载] 本页已预加载: ${preloadCount}`);
                }, 2500);
            }
        } catch (e) {
            if (config.debug) console.warn('[Preload] 异常:', e);
        }
    };

    // 布局稳定性优化
    const initLayoutStabilization = () => {
        if (optimizerSiteBlacklist.some(domain => location.hostname.includes(domain)) ||
            layoutSiteBlacklist.some(domain => location.hostname.includes(domain))) {
            if (config.debug) console.log('[布局优化] 当前站点已在全局或布局黑名单,跳过布局优化');
            return;
        }
        try {
            const styles = [];
            if (config.features.layout.stableImages) {
                styles.push(`
                    img:not([width]):not([height]) {
                        min-height: 1px;
                        // display: block;
                        // max-width: 100%;
                    }
                    @supports (aspect-ratio: 1/1) {
                        img:not([width]):not([height]) {
                            aspect-ratio: attr(width) / attr(height);
                        }
                    }
                `);
            }
            if (config.features.layout.stableIframes) {
                styles.push(`
                    iframe:not([width]):not([height]) {
                        // width: 100%;
                        height: auto;
                        // aspect-ratio: 16/9;
                        // display: block;
                    }
                `);
            }
            if (styles.length) {
                const style = document.createElement('style');
                style.textContent = styles.join('\n');
                document.head.appendChild(style);
                if (config.debug) {
                    console.log('[布局优化] 样式已注入');
                }
            }
        } catch (e) {
            if (config.debug) console.warn('[Layout] 异常:', e);
        }
    };

    // ========================
    // 工具函数
    // ========================
    function throttle(func, limit) {
        let inThrottle;
        return function() {
            if (!inThrottle) {
                func.apply(this, arguments);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        };
    }

    // ========================
    // 初始化系统
    // ========================
    document.addEventListener('DOMContentLoaded', () => {
        initSmartPreconnect();
        initResourcePreload();
        initLazyLoad();
        initLayoutStabilization();

        if (config.debug) {
            console.groupCollapsed('[Optimizer] 初始化报告');
            console.log('激活功能:', Object.entries(config.features)
                .filter(([_, v]) => v.enabled !== false)
                .map(([k]) => k));
            console.log('预连接白名单:', config.features.preconnect.whitelist);
            console.log('预加载类型:', config.features.preload.types);
            console.groupEnd();
        }
    });
})();