WebP加载优化(此脚本转为图片加载而设计)

智能图片加载优化:原生懒加载、WebP优先、连接感知、错误恢复、崩溃防护

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WebP加载优化(此脚本转为图片加载而设计)
// @namespace    http://tampermonkey.net/
// @version      3.0.3
// @description  智能图片加载优化:原生懒加载、WebP优先、连接感知、错误恢复、崩溃防护
// @author       KiwiFruit
// @match        *://*/*
// @grant        none
// @license      MIT
// ==/UserScript==
(function() {
    'use strict';

    // ==================== 浏览器能力检测 ====================
    const browserCapabilities = {
        supportsLazyLoading: 'loading' in HTMLImageElement.prototype,
        supportsWebP: (() => {
            try {
                const canvas = document.createElement('canvas');
                return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
            } catch (error) {
                console.warn('[WebP检测] 检测失败:', error.message);
                return false;
            }
        })(),
        supportsIntersectionObserver: 'IntersectionObserver' in window,
        supportsMutationObserver: 'MutationObserver' in window,
        supportsPerformanceObserver: 'PerformanceObserver' in window,
    };

    // ==================== 配置参数 ====================
    const config = {
        rootMargin: '200px 0px',
        threshold: 0.01,
        connectionAware: true,
        slowConnectionThreshold: '2g',
        maxConcurrentLoads: 6,
        retryLimit: 2,
        retryDelay: 3000,
        webpQuality: 75,
        jpegQuality: 85,
        placeholder: 'data:image/svg+xml;base64,' +
            btoa('<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">' +
                '<rect width="100%" height="100%" fill="#f0f0f0"/>' +
                '</svg>'),
        enablePerformanceMonitoring: true,
        logLevel: 'warn',
    };

    // ==================== 全局状态 ====================
    const state = {
        activeLoads: 0,
        totalImages: 0,
        loadedImages: 0,
        failedImages: 0,
        isPageVisible: true,
        isSlowConnection: false,
        observer: null,
        mutationObserver: null,
        networkConnection: null,
        performanceEntries: [],
        timeouts: new Set(), // 用于清理定时器
    };

    // ==================== 工具函数 ====================
    const utils = {
        throttle(fn, delay) {
            let lastCall = 0;
            return function(...args) {
                const now = Date.now();
                if (now - lastCall >= delay) {
                    lastCall = now;
                    fn.apply(this, args);
                }
            };
        },
        debounce(fn, delay) {
            let timeoutId;
            return function(...args) {
                clearTimeout(timeoutId);
                timeoutId = setTimeout(() => fn.apply(this, args), delay);
            };
        },
        isValidUrl(str) {
            if (!str || typeof str !== 'string') return false;
            try {
                const url = new URL(str, window.location.href);
                return url.protocol === 'http:' || url.protocol === 'https:';
            } catch {
                return false;
            }
        },
        isInViewport(element, offset = 0) {
            if (!element || !element.getBoundingClientRect) return false;
            const rect = element.getBoundingClientRect();
            const windowHeight = window.innerHeight || document.documentElement.clientHeight;
            const windowWidth = window.innerWidth || document.documentElement.clientWidth;
            return (
                rect.top - offset <= windowHeight &&
                rect.bottom + offset >= 0 &&
                rect.left - offset <= windowWidth &&
                rect.right + offset >= 0
            );
        },
        isCriticalImage(img) {
            if (!img || !img.getBoundingClientRect) return false;
            const viewportHeight = window.innerHeight;
            const rect = img.getBoundingClientRect();
            return rect.top < viewportHeight * 1.5;
        },
        getOptimalImageSource(img) {
            if (!img || img.nodeType !== Node.ELEMENT_NODE || img.tagName !== 'IMG') return null;
            const ds = img.dataset;
            if (browserCapabilities.supportsWebP && ds.webpSrc && utils.isValidUrl(ds.webpSrc)) {
                return ds.webpSrc;
            }
            if (ds.src && utils.isValidUrl(ds.src)) {
                return ds.src;
            }
            if (img.src && !img.src.startsWith('data:') && utils.isValidUrl(img.src)) {
                return img.src;
            }
            return null;
        },
        log(level, message, data = null) {
            const levels = ['debug', 'info', 'warn', 'error'];
            const currentLevelIndex = levels.indexOf(config.logLevel);
            const messageLevelIndex = levels.indexOf(level);
            if (messageLevelIndex >= currentLevelIndex) {
                const prefix = `[图片优化 ${new Date().toISOString()}]`;
                if (data) {
                    console[level](prefix, message, data);
                } else {
                    console[level](prefix, message);
                }
            }
        },
        generatePerformanceReport() {
            if (!config.enablePerformanceMonitoring || state.performanceEntries.length === 0) {
                return null;
            }
            const validTimes = state.performanceEntries
                .filter(entry => typeof entry.loadTime === 'number')
                .map(entry => entry.loadTime);
            const report = {
                total: state.totalImages,
                loaded: state.loadedImages,
                failed: state.failedImages,
                successRate: state.totalImages > 0 ? ((state.loadedImages / state.totalImages) * 100).toFixed(2) + '%' : 'N/A',
                averageLoadTime: validTimes.length > 0 ? (validTimes.reduce((a, b) => a + b, 0) / validTimes.length).toFixed(2) + 'ms' : 'N/A',
                performanceEntries: state.performanceEntries,
            };
            utils.log('info', '性能报告', report);
            return report;
        },
        safeCall(fn, ...args) {
            try {
                if (typeof fn === 'function') fn(...args);
            } catch (e) {
                utils.log('error', '安全调用失败', e);
            }
        },
    };

    // ==================== 网络连接检测 ====================
    const networkMonitor = {
        init() {
            if (!config.connectionAware || !navigator.connection) {
                state.isSlowConnection = false;
                utils.log('info', '网络感知未启用或浏览器不支持');
                return;
            }
            state.networkConnection = navigator.connection ||
                                     navigator.mozConnection ||
                                     navigator.webkitConnection;
            this.updateConnectionStatus();
            if (state.networkConnection) {
                state.networkConnection.addEventListener('change', utils.debounce(() => {
                    this.updateConnectionStatus();
                    this.adjustConfiguration();
                }, 500));
            }
        },
        updateConnectionStatus() {
            if (!state.networkConnection) return;
            const effectiveType = state.networkConnection.effectiveType;
            const saveData = state.networkConnection.saveData;
            state.isSlowConnection = saveData ||
                                   effectiveType === '2g' ||
                                   effectiveType === 'slow-2g' ||
                                   effectiveType === config.slowConnectionThreshold;
            document.documentElement.classList.remove('network-fast', 'network-slow', 'save-data');
            if (saveData) document.documentElement.classList.add('save-data');
            if (state.isSlowConnection) {
                document.documentElement.classList.add('network-slow');
                utils.log('info', '检测到慢速网络连接', { effectiveType, saveData });
            } else {
                document.documentElement.classList.add('network-fast');
            }
        },
        adjustConfiguration() {
            if (state.isSlowConnection) {
                config.rootMargin = '50px 0px';
                config.maxConcurrentLoads = 2;
            } else {
                config.rootMargin = '200px 0px';
                config.maxConcurrentLoads = 18;
            }
            if (state.observer) {
                state.observer.disconnect();
                observerManager.initIntersectionObserver();
            }
        },
        getNetworkAwareImageSource(img) {
            const lowQuality = img.dataset.lowQualitySrc;
            if (state.isSlowConnection && lowQuality && utils.isValidUrl(lowQuality)) {
                return lowQuality;
            }
            return utils.getOptimalImageSource(img);
        },
    };

    // ==================== 样式管理 ====================
    const styleManager = {
        injectStyles() {
            if (document.getElementById('webp-optimizer-styles')) return;
            const style = document.createElement('style');
            style.id = 'webp-optimizer-styles';
            style.textContent = this.getStyles();
            document.head.appendChild(style);
            utils.log('debug', '样式已注入');
        },
        getStyles() {
            return `
                img[data-src], img[data-webp-src], img[data-low-quality-src] {
                    opacity: 0; transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
                    background-color: #f5f5f5;
                    background-image: linear-gradient(90deg, #f0f0f0 0%, #f0f0f0 40%, #e0e0e0 50%, #f0f0f0 60%, #f0f0f0 100%);
                    background-size: 200% 100%; animation: shimmer 1.5s infinite; will-change: opacity;
                }
                img[data-src][data-width][data-height], img[data-webp-src][data-width][data-height] {
                    aspect-ratio: attr(data-width) / attr(data-height);
                }
                @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
                img.webp-optimizer-loaded { opacity: 1 !important; background: none !important; animation: none !important; }
                img.webp-optimizer-error { opacity: 0.7; background: #ffebee !important; position: relative; animation: none !important; }
                img.webp-optimizer-error::after {
                    content: "图片加载失败";
                    position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
                    color: #d32f2f; font-size: 12px; font-family: -apple-system, BlinkMacSystemFont, sans-serif;
                    background: rgba(255,255,255,0.9); padding: 4px 8px; border-radius: 4px; border: 1px solid #ffcdd2;
                }
                .network-slow img[data-src], .network-slow img[data-webp-src] {
                    background-size: 400% 100%; animation-duration: 2.5s;
                }
                img[data-priority="high"] { animation: pulse-highlight 2s ease-in-out; }
                @keyframes pulse-highlight {
                    0%,100% { box-shadow: 0 0 0 0 rgba(33,150,243,0); }
                    50% { box-shadow: 0 0 0 2px rgba(33,150,243,0.3); }
                }
            `;
        },
    };

    // ==================== 图片加载器 ====================
    const imageLoader = {
        async loadImage(img) {
            if (!img || !document.contains(img) || img.dataset.loaded === 'true' || img.complete) {
                return Promise.resolve();
            }
            await this.waitForLoadSlot();
            const startTime = performance.now();
            const originalSrc = img.src;
            const bestSrc = networkMonitor.getNetworkAwareImageSource(img);
            if (!bestSrc) {
                utils.log('warn', '未找到合适的图片源', img);
                return Promise.reject(new Error('No suitable image source found'));
            }
            if (img.src === bestSrc && img.complete) return Promise.resolve();

            img.dataset.loading = 'true';
            state.activeLoads++;

            return new Promise((resolve, reject) => {
                let isResolved = false;
                const cleanup = () => {
                    isResolved = true;
                    state.timeouts.forEach(id => clearTimeout(id));
                    state.timeouts.clear();
                };

                const timeoutId = setTimeout(() => {
                    if (!isResolved) {
                        cleanup();
                        this.handleImageError(img, new Error('Image load timeout'), originalSrc);
                        reject(new Error('Load timeout'));
                    }
                }, 30000);
                state.timeouts.add(timeoutId);

                const image = new Image();
                image.onload = () => {
                    if (isResolved || !document.contains(img)) return;
                    cleanup();
                    const loadTime = performance.now() - startTime;
                    img.src = bestSrc;
                    img.dataset.loaded = 'true';
                    img.dataset.loading = 'false';
                    img.classList.add('webp-optimizer-loaded');
                    img.classList.remove('webp-optimizer-error');
                    delete img.dataset.src;
                    delete img.dataset.webpSrc;
                    state.activeLoads--;
                    state.loadedImages++;
                    // 限制性能条目数
                    if (state.performanceEntries.length >= 2000) state.performanceEntries.shift();
                    state.performanceEntries.push({
                        src: bestSrc,
                        loadTime,
                        timestamp: Date.now(),
                        size: this.getImageSize(image),
                        networkType: state.networkConnection?.effectiveType,
                    });
                    utils.log('debug', `图片加载完成: ${bestSrc}`, { loadTime: `${loadTime.toFixed(2)}ms` });
                    resolve();
                };
                image.onerror = (error) => {
                    if (isResolved || !document.contains(img)) return;
                    cleanup();
                    this.handleImageError(img, error, originalSrc);
                    reject(error);
                };

                // WebP 降级
                if (bestSrc.includes('.webp') && img.dataset.src && utils.isValidUrl(img.dataset.src)) {
                    image.onerror = () => {
                        if (!isResolved) {
                            utils.log('info', 'WebP加载失败,尝试降级', { webp: bestSrc, fallback: img.dataset.src });
                            image.src = img.dataset.src;
                        }
                    };
                }
                image.src = bestSrc;
            });
        },
        waitForLoadSlot() {
            return new Promise(resolve => {
                const check = () => {
                    if (state.activeLoads < config.maxConcurrentLoads) {
                        resolve();
                    } else {
                        const id = setTimeout(check, 50);
                        state.timeouts.add(id);
                    }
                };
                check();
            });
        },
        handleImageError(img, error, originalSrc) {
            if (!img || !document.contains(img)) return;
            state.activeLoads--;
            state.failedImages++;
            const retryCount = parseInt(img.dataset.retryCount || '0', 10);
            img.classList.add('webp-optimizer-error');
            img.classList.remove('webp-optimizer-loaded');
            img.dataset.loading = 'false';
            utils.log('warn', '图片加载失败', { src: img.src || img.dataset.src, error: error.message, retryCount });

            if (retryCount < config.retryLimit) {
                img.dataset.retryCount = retryCount + 1;
                const delay = Math.min(30000, 1000 * Math.pow(2, retryCount)); // 指数退避
                const id = setTimeout(() => {
                    if (document.contains(img) && !img.dataset.loaded) {
                        this.loadImage(img).catch(() => {
                            if (originalSrc && originalSrc !== config.placeholder && document.contains(img)) {
                                img.src = originalSrc;
                            }
                        });
                    }
                }, delay);
                state.timeouts.add(id);
            } else if (originalSrc && originalSrc !== config.placeholder && document.contains(img)) {
                img.src = originalSrc;
            }
        },
        getImageSize(img) {
            if (!img || !img.naturalWidth) return 'unknown';
            return `${img.naturalWidth}×${img.naturalHeight}`;
        },
    };

    // ==================== 观察者管理 ====================
    const observerManager = {
        initIntersectionObserver() {
            if (!browserCapabilities.supportsIntersectionObserver) {
                utils.log('warn', '浏览器不支持IntersectionObserver');
                return;
            }
            state.observer = new IntersectionObserver(this.handleIntersection.bind(this), {
                rootMargin: config.rootMargin,
                threshold: config.threshold,
                root: null,
            });
        },
        handleIntersection(entries) {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const img = entry.target;
                    if (img && img.tagName === 'IMG') {
                        state.observer.unobserve(img);
                        if (img.dataset.loaded !== 'true' && img.dataset.loading !== 'true') {
                            imageLoader.loadImage(img).catch(e => utils.log('error', '懒加载失败', { src: img.src, error: e.message }));
                        }
                    }
                }
            });
        },
        initMutationObserver() {
            if (!browserCapabilities.supportsMutationObserver) return;
            const debouncedProcess = utils.debounce(this.processNewNodes.bind(this), 100);
            state.mutationObserver = new MutationObserver(mutations => {
                for (const mutation of mutations) {
                    if (mutation.type === 'childList' && mutation.addedNodes.length) {
                        debouncedProcess(mutation.addedNodes);
                    }
                }
            });
            state.mutationObserver.observe(document.body, { childList: true, subtree: true });
        },
        processNewNodes(nodes) {
            const newImages = [];
            for (const node of nodes) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node.tagName === 'IMG') {
                        newImages.push(node);
                    } else if (node.querySelectorAll) {
                        const imgs = node.querySelectorAll('img[data-src], img[data-webp-src], img[data-low-quality-src]');
                        for (const img of imgs) newImages.push(img);
                    }
                }
            }
            if (newImages.length) {
                utils.log('debug', `检测到${newImages.length}个新图片`);
                for (const img of newImages) imageProcessor.process(img);
            }
        },
        initPerformanceObserver() {
            if (!config.enablePerformanceMonitoring || !browserCapabilities.supportsPerformanceObserver) return;
            try {
                const perfObserver = new PerformanceObserver(list => {
                    for (const entry of list.getEntries()) {
                        if (entry.entryType === 'resource' && entry.initiatorType === 'img') {
                            utils.log('debug', '资源加载性能', entry);
                        }
                    }
                });
                perfObserver.observe({ entryTypes: ['resource'] });
            } catch (error) {
                utils.log('warn', '性能观察者初始化失败', error.message);
            }
        },
        cleanup() {
            if (state.observer) {
                state.observer.disconnect();
                state.observer = null;
            }
            if (state.mutationObserver) {
                state.mutationObserver.disconnect();
                state.mutationObserver = null;
            }
            for (const id of state.timeouts) clearTimeout(id);
            state.timeouts.clear();
            utils.log('debug', '观察者及定时器已清理');
        },
    };

    // ==================== 图片处理器 ====================
    const imageProcessor = {
        process(img) {
            if (!img || img.nodeType !== Node.ELEMENT_NODE || img.tagName !== 'IMG' || img.dataset.processed === 'true') {
                return;
            }
            state.totalImages++;
            img.dataset.processed = 'true';

            if (!img.src || img.src === '' || img.src.startsWith('data:')) {
                img.src = config.placeholder;
                if (img.dataset.width && img.dataset.height) {
                    img.setAttribute('width', img.dataset.width);
                    img.setAttribute('height', img.dataset.height);
                }
            }

            const isCritical = utils.isCriticalImage(img);
            this.applyLoadingStrategy(img, isCritical);
        },
        applyLoadingStrategy(img, isCritical) {
            if (!document.contains(img)) return;

            if (isCritical) {
                img.dataset.priority = 'high';
                imageLoader.loadImage(img).catch(e => utils.log('error', '关键图片加载失败', { src: img.src, error: e.message }));
                return;
            }

            if (browserCapabilities.supportsLazyLoading) {
                img.loading = 'lazy';
                if (utils.isInViewport(img, 100)) {
                    imageLoader.loadImage(img).catch(e => utils.log('error', '视口内图片加载失败', { src: img.src, error: e.message }));
                } else {
                    const bestSrc = networkMonitor.getNetworkAwareImageSource(img);
                    if (bestSrc && img.src !== bestSrc) img.src = bestSrc;
                }
            } else if (browserCapabilities.supportsIntersectionObserver) {
                img.loading = 'eager';
                state.observer.observe(img);
            } else {
                imageLoader.loadImage(img).catch(e => utils.log('error', '回退加载失败', { src: img.src, error: e.message }));
            }
        },
        processAllImages() {
            const images = document.querySelectorAll('img[data-src], img[data-webp-src], img[data-low-quality-src]');
            utils.log('info', `找到${images.length}个待处理图片`);
            for (const img of images) {
                try {
                    this.process(img);
                } catch (error) {
                    utils.log('error', '图片处理失败', { img, error: error.message });
                }
            }
        },
    };

    // ==================== 页面生命周期管理 ====================
    const pageLifecycle = {
        init() {
            document.addEventListener('visibilitychange', () => {
                state.isPageVisible = !document.hidden;
                if (state.isPageVisible) this.resumeLoading();
            });
            window.addEventListener('beforeunload', () => {
                observerManager.cleanup();
                utils.generatePerformanceReport();
                utils.log('info', '页面卸载,脚本清理完成');
            });
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => this.handleDOMContentLoaded());
            } else {
                this.handleDOMContentLoaded();
            }
            window.addEventListener('resize', utils.debounce(() => this.reassessCriticalImages(), 250));
        },
        handleDOMContentLoaded() {
            utils.log('info', 'DOM加载完成,开始处理图片');
            imageProcessor.processAllImages();
        },
        reassessCriticalImages() {
            const images = document.querySelectorAll('img[data-processed="true"]:not([data-loaded="true"])');
            for (const img of images) {
                if (document.contains(img) && utils.isCriticalImage(img) && img.dataset.loading !== 'true') {
                    imageLoader.loadImage(img).catch(e => utils.log('error', '重新评估后加载失败', { src: img.src, error: e.message }));
                }
            }
        },
        resumeLoading() {
            this.reassessCriticalImages();
        },
        pauseLoading() {
            // 可扩展
        },
    };

    // ==================== 主初始化函数 ====================
    function init() {
        utils.log('info', 'WebP图片优化脚本(加固版)初始化', { capabilities: browserCapabilities, config });
        styleManager.injectStyles();
        networkMonitor.init();
        observerManager.initIntersectionObserver();
        observerManager.initMutationObserver();
        observerManager.initPerformanceObserver();
        pageLifecycle.init();
        window.addEventListener('unload', () => observerManager.cleanup());
        setTimeout(() => {
            utils.log('info', 'WebP图片优化脚本初始化完成', { totalImages: state.totalImages });
        }, 1000);
    }

    // ==================== 公共API ====================
    window.WebPOptimizer = {
        version: '3.1.0',
        refresh() {
            utils.log('info', '手动刷新图片处理');
            imageProcessor.processAllImages();
        },
        loadImage(img) {
            return imageLoader.loadImage(img);
        },
        getStats() {
            return {
                total: state.totalImages,
                loaded: state.loadedImages,
                failed: state.failedImages,
                active: state.activeLoads,
                successRate: state.totalImages > 0 ? (state.loadedImages / state.totalImages * 100).toFixed(2) + '%' : '0%',
                performanceReport: utils.generatePerformanceReport(),
            };
        },
        updateConfig(newConfig) {
            Object.assign(config, newConfig);
            utils.log('info', '配置已更新', newConfig);
            if (newConfig.connectionAware !== undefined) networkMonitor.init();
        },
        destroy() {
            observerManager.cleanup();
            const style = document.getElementById('webp-optimizer-styles');
            if (style) style.remove();
            delete window.WebPOptimizer;
            utils.log('info', '脚本已销毁');
        },
    };

    // ==================== 启动 ====================
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 0);
    }
})();