智能图片加载优化:原生懒加载、WebP优先、连接感知、错误恢复、崩溃防护
// ==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);
}
})();