Ultimate Web Optimizer

全面的网页性能优化方案- 懒加载/预加载/预连接/布局优化

  1. // ==UserScript==
  2. // @name Ultimate Web Optimizer
  3. // @namespace https://greasyfork.org/zh-CN/users/1474228-moyu001
  4. // @version 2.1
  5. // @description 全面的网页性能优化方案- 懒加载/预加载/预连接/布局优化
  6. // @author moyu001
  7. // @match *://*/*
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_log
  11. // @license MIT
  12. // @run-at document-start
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // ========================
  19. // 工具函数 - 提前定义
  20. // ========================
  21.  
  22. /**
  23. * 防抖函数
  24. * @param {Function} fn 要防抖的函数
  25. * @param {number} delay 延迟时间
  26. * @returns {Function} 防抖后的函数
  27. */
  28. function debounce(fn, delay) {
  29. let timer = null;
  30. return function(...args) {
  31. clearTimeout(timer);
  32. timer = setTimeout(() => fn.apply(this, args), delay);
  33. };
  34. }
  35.  
  36. /**
  37. * 节流函数
  38. * @param {Function} func 要节流的函数
  39. * @param {number} limit 限制时间
  40. * @returns {Function} 节流后的函数
  41. */
  42. function throttle(func, limit) {
  43. let inThrottle;
  44. return function(...args) {
  45. if (!inThrottle) {
  46. func.apply(this, args);
  47. inThrottle = true;
  48. setTimeout(() => inThrottle = false, limit);
  49. }
  50. };
  51. }
  52.  
  53. /**
  54. * 安全的 URL 解析
  55. * @param {string} url URL 字符串
  56. * @param {string} base 基准 URL
  57. * @returns {URL|null} URL 对象或 null
  58. */
  59. function safeParseURL(url, base) {
  60. try {
  61. return new URL(url, base);
  62. } catch {
  63. return null;
  64. }
  65. }
  66.  
  67. /**
  68. * 检查元素是否可见
  69. * @param {Element} element 要检查的元素
  70. * @returns {boolean} 是否可见
  71. */
  72. function isElementVisible(element) {
  73. if (!element) return false;
  74. const style = window.getComputedStyle(element);
  75. return style.display !== 'none' &&
  76. style.visibility !== 'hidden' &&
  77. style.opacity !== '0';
  78. }
  79.  
  80. /**
  81. * 深度合并对象
  82. * @param {Object} target 目标对象
  83. * @param {Object} source 源对象
  84. * @returns {Object} 合并后的对象
  85. */
  86. function deepMerge(target, source) {
  87. const result = { ...target };
  88.  
  89. for (const key in source) {
  90. if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
  91. result[key] = deepMerge(result[key] || {}, source[key]);
  92. } else {
  93. result[key] = source[key];
  94. }
  95. }
  96.  
  97. return result;
  98. }
  99.  
  100. /**
  101. * 检查是否为 HTML 图片元素 - 增强类型检查
  102. * @param {Node} node DOM 节点
  103. * @returns {boolean} 是否为图片元素
  104. */
  105. function isImageElement(node) {
  106. return node instanceof HTMLImageElement;
  107. }
  108.  
  109. /**
  110. * 延迟函数
  111. * @param {number} ms 延迟毫秒数
  112. * @returns {Promise} Promise 对象
  113. */
  114. function delay(ms) {
  115. return new Promise(resolve => setTimeout(resolve, ms));
  116. }
  117.  
  118. /**
  119. * 简单的 LRU 缓存实现
  120. */
  121. class LRUCache {
  122. constructor(maxSize = 100) {
  123. this.maxSize = maxSize;
  124. this.cache = new Map();
  125. }
  126.  
  127. get(key) {
  128. if (this.cache.has(key)) {
  129. const value = this.cache.get(key);
  130. this.cache.delete(key);
  131. this.cache.set(key, value);
  132. return value;
  133. }
  134. return null;
  135. }
  136.  
  137. set(key, value) {
  138. if (this.cache.has(key)) {
  139. this.cache.delete(key);
  140. } else if (this.cache.size >= this.maxSize) {
  141. const firstKey = this.cache.keys().next().value;
  142. this.cache.delete(firstKey);
  143. }
  144. this.cache.set(key, value);
  145. }
  146.  
  147. has(key) {
  148. return this.cache.has(key);
  149. }
  150.  
  151. clear() {
  152. this.cache.clear();
  153. }
  154.  
  155. get size() {
  156. return this.cache.size;
  157. }
  158. }
  159.  
  160. /**
  161. * 重试操作工具类 - 新增错误重试机制
  162. */
  163. class RetryableOperation {
  164. /**
  165. * 执行带重试的操作
  166. * @param {Function} operation 要执行的操作
  167. * @param {number} maxRetries 最大重试次数
  168. * @param {number} baseDelay 基础延迟时间
  169. * @returns {Promise} 操作结果
  170. */
  171. static async executeWithRetry(operation, maxRetries = 3, baseDelay = 1000) {
  172. for (let i = 0; i < maxRetries; i++) {
  173. try {
  174. return await operation();
  175. } catch (e) {
  176. if (i === maxRetries - 1) throw e;
  177. await delay(baseDelay * (i + 1));
  178. }
  179. }
  180. }
  181. }
  182.  
  183. /**
  184. * 性能监控器
  185. */
  186. class PerformanceMonitor {
  187. constructor(debug = false) {
  188. this.debug = debug;
  189. this.metrics = new Map();
  190. this.counters = new Map();
  191. }
  192.  
  193. start(name) {
  194. if (this.debug) {
  195. this.metrics.set(name, performance.now());
  196. }
  197. }
  198.  
  199. end(name) {
  200. if (this.debug && this.metrics.has(name)) {
  201. const duration = performance.now() - this.metrics.get(name);
  202. console.log(`[性能] ${name}: ${duration.toFixed(2)}ms`);
  203. this.metrics.delete(name);
  204. return duration;
  205. }
  206. return 0;
  207. }
  208.  
  209. count(name) {
  210. if (this.debug) {
  211. this.counters.set(name, (this.counters.get(name) || 0) + 1);
  212. }
  213. }
  214.  
  215. getCounter(name) {
  216. return this.counters.get(name) || 0;
  217. }
  218.  
  219. profile(name, fn) {
  220. if (!this.debug) return fn();
  221.  
  222. const start = performance.now();
  223. const result = fn();
  224. const end = performance.now();
  225. console.log(`[性能] ${name}: ${(end - start).toFixed(2)}ms`);
  226. return result;
  227. }
  228.  
  229. log(message, ...args) {
  230. if (this.debug) {
  231. console.log(`[优化器] ${message}`, ...args);
  232. }
  233. }
  234.  
  235. warn(message, ...args) {
  236. if (this.debug) {
  237. console.warn(`[优化器] ${message}`, ...args);
  238. }
  239. }
  240.  
  241. error(message, ...args) {
  242. if (this.debug) {
  243. console.error(`[优化器] ${message}`, ...args);
  244. }
  245. }
  246. }
  247.  
  248. // ========================
  249. // 配置管理系统
  250. // ========================
  251.  
  252. /**
  253. * 配置管理器 - 使用深度合并
  254. */
  255. class ConfigManager {
  256. constructor() {
  257. this.defaultConfig = {
  258. debug: false,
  259.  
  260. // 全局黑名单
  261. globalBlacklist: [
  262. // 可以添加需要完全跳过优化的域名
  263. ],
  264.  
  265. // 懒加载配置
  266. lazyLoad: {
  267. enabled: true,
  268. minSize: 100,
  269. rootMargin: '200px',
  270. threshold: 0.01,
  271. skipHidden: true,
  272. batchSize: 32,
  273. blacklist: []
  274. },
  275.  
  276. // 预连接配置
  277. preconnect: {
  278. enabled: true,
  279. maxConnections: 5,
  280. whitelist: [
  281. // CDN 和字体服务
  282. 'fonts.gstatic.com',
  283. 'fonts.googleapis.com',
  284. 'fonts.googleapis.cn',
  285. 'fonts.loli.net',
  286.  
  287. // 常用 CDN
  288. 'cdnjs.cloudflare.com',
  289. 'cdn.jsdelivr.net',
  290. 'unpkg.com',
  291. 'cdn.bootcdn.net',
  292. 'cdn.bootcss.com',
  293. 'libs.baidu.com',
  294. 'cdn.staticfile.org',
  295.  
  296. // 其他常用服务
  297. 'ajax.googleapis.com',
  298. 'code.jquery.com',
  299. 'maxcdn.bootstrapcdn.com',
  300. 'kit.fontawesome.com',
  301. 'lf3-cdn-tos.bytecdntp.com',
  302. 'unpkg.zhimg.com',
  303. 'npm.elemecdn.com',
  304. 'g.alicdn.com'
  305. ],
  306. blacklist: []
  307. },
  308.  
  309. // 预加载配置
  310. preload: {
  311. enabled: true,
  312. maxPreloads: 5,
  313. types: ['css', 'js', 'woff2', 'woff'],
  314. fetchTimeout: 5000,
  315. retryAttempts: 3,
  316. blacklist: []
  317. },
  318.  
  319. // 布局稳定性配置
  320. layout: {
  321. enabled: true,
  322. stableImages: true,
  323. stableIframes: true,
  324. blacklist: []
  325. }
  326. };
  327.  
  328. this.config = this.loadConfig();
  329. this.validateConfig();
  330. }
  331.  
  332. loadConfig() {
  333. try {
  334. const saved = GM_getValue('optimizer_config_v2', null);
  335. if (saved) {
  336. // 使用深度合并替代浅合并
  337. return deepMerge(this.defaultConfig, JSON.parse(saved));
  338. }
  339. } catch (e) {
  340. console.warn('[配置] 加载用户配置失败,使用默认配置', e);
  341. }
  342. return deepMerge({}, this.defaultConfig);
  343. }
  344.  
  345. saveConfig() {
  346. try {
  347. GM_setValue('optimizer_config_v2', JSON.stringify(this.config));
  348. } catch (e) {
  349. console.warn('[配置] 保存配置失败', e);
  350. }
  351. }
  352.  
  353. validateConfig() {
  354. // 基本类型验证
  355. if (typeof this.config.debug !== 'boolean') {
  356. this.config.debug = this.defaultConfig.debug;
  357. }
  358.  
  359. // 确保数组类型配置正确
  360. ['globalBlacklist'].forEach(key => {
  361. if (!Array.isArray(this.config[key])) {
  362. this.config[key] = [...this.defaultConfig[key]];
  363. }
  364. });
  365.  
  366. // 验证子配置
  367. ['lazyLoad', 'preconnect', 'preload', 'layout'].forEach(module => {
  368. if (!this.config[module] || typeof this.config[module] !== 'object') {
  369. this.config[module] = deepMerge({}, this.defaultConfig[module]);
  370. }
  371. });
  372. }
  373.  
  374. get(path) {
  375. const keys = path.split('.');
  376. let value = this.config;
  377. for (const key of keys) {
  378. value = value?.[key];
  379. if (value === undefined) break;
  380. }
  381. return value;
  382. }
  383.  
  384. set(path, value) {
  385. const keys = path.split('.');
  386. const lastKey = keys.pop();
  387. let target = this.config;
  388.  
  389. for (const key of keys) {
  390. if (!target[key] || typeof target[key] !== 'object') {
  391. target[key] = {};
  392. }
  393. target = target[key];
  394. }
  395.  
  396. target[lastKey] = value;
  397. this.saveConfig();
  398. }
  399.  
  400. isBlacklisted(hostname, module = null) {
  401. // 检查全局黑名单
  402. if (this.config.globalBlacklist.some(domain => hostname.includes(domain))) {
  403. return true;
  404. }
  405.  
  406. // 检查模块特定黑名单
  407. if (module && this.config[module]?.blacklist) {
  408. return this.config[module].blacklist.some(domain => hostname.includes(domain));
  409. }
  410.  
  411. return false;
  412. }
  413. }
  414.  
  415. // ========================
  416. // 核心优化模块
  417. // ========================
  418.  
  419. /**
  420. * 懒加载管理器 - 增强类型检查
  421. */
  422. class LazyLoadManager {
  423. constructor(config, monitor) {
  424. this.config = config;
  425. this.monitor = monitor;
  426. this.observer = null;
  427. this.mutationObserver = null;
  428. this.processedImages = new Set();
  429. this.pendingImages = [];
  430. this.batchScheduled = false;
  431. this.processedElements = new WeakSet(); // 避免重复处理
  432. }
  433.  
  434. init() {
  435. if (!this.config.get('lazyLoad.enabled')) {
  436. this.monitor.log('懒加载功能已禁用');
  437. return;
  438. }
  439.  
  440. if (this.config.isBlacklisted(location.hostname, 'lazyLoad')) {
  441. this.monitor.log('当前站点在懒加载黑名单中');
  442. return;
  443. }
  444.  
  445. this.monitor.start('lazyLoad-init');
  446. this.setupIntersectionObserver();
  447. this.processExistingImages();
  448. this.setupMutationObserver();
  449. this.monitor.end('lazyLoad-init');
  450. }
  451.  
  452. setupIntersectionObserver() {
  453. if (!('IntersectionObserver' in window)) {
  454. this.monitor.warn('浏览器不支持 IntersectionObserver,使用兼容模式');
  455. this.setupFallbackMode();
  456. return;
  457. }
  458.  
  459. this.observer = new IntersectionObserver((entries) => {
  460. entries.forEach(entry => {
  461. if (entry.isIntersecting) {
  462. this.restoreImage(entry.target);
  463. this.observer.unobserve(entry.target);
  464. this.monitor.count('lazy-loaded-images');
  465. }
  466. });
  467. }, {
  468. rootMargin: this.config.get('lazyLoad.rootMargin'),
  469. threshold: this.config.get('lazyLoad.threshold')
  470. });
  471. }
  472.  
  473. setupFallbackMode() {
  474. const checkVisible = throttle(() => {
  475. const images = document.querySelectorAll('img[data-lazy-src]');
  476. const margin = parseInt(this.config.get('lazyLoad.rootMargin')) || 200;
  477.  
  478. images.forEach(img => {
  479. const rect = img.getBoundingClientRect();
  480. if (rect.top < window.innerHeight + margin) {
  481. this.restoreImage(img);
  482. }
  483. });
  484. }, 200);
  485.  
  486. window.addEventListener('scroll', checkVisible, { passive: true });
  487. window.addEventListener('resize', checkVisible, { passive: true });
  488. checkVisible();
  489. }
  490.  
  491. isLazyCandidate(img) {
  492. // 基本检查 - 使用更严格的类型检查
  493. if (!isImageElement(img)) return false;
  494. if (this.processedElements.has(img)) return false;
  495. if (img.hasAttribute('data-lazy-processed')) return false;
  496. if (img.loading === 'eager') return false;
  497. if (img.complete && img.src) return false;
  498. if (img.src && img.src.startsWith('data:')) return false;
  499.  
  500. // 跳过已有懒加载的图片
  501. if (img.hasAttribute('data-src') || img.hasAttribute('data-srcset')) return false;
  502.  
  503. // 尺寸检查
  504. const minSize = this.config.get('lazyLoad.minSize');
  505. const rect = img.getBoundingClientRect();
  506. if (rect.width < minSize || rect.height < minSize) return false;
  507.  
  508. // 可见性检查
  509. if (this.config.get('lazyLoad.skipHidden') && !isElementVisible(img)) {
  510. return false;
  511. }
  512.  
  513. return true;
  514. }
  515.  
  516. processImage(img) {
  517. if (!this.isLazyCandidate(img)) return false;
  518.  
  519. // 标记为已处理
  520. this.processedElements.add(img);
  521. img.setAttribute('data-lazy-processed', 'true');
  522.  
  523. // 设置原生懒加载(如果支持)
  524. if ('loading' in HTMLImageElement.prototype) {
  525. img.loading = 'lazy';
  526. }
  527.  
  528. // 保存原始 src
  529. if (img.src) {
  530. img.setAttribute('data-lazy-src', img.src);
  531. img.removeAttribute('src');
  532. }
  533. if (img.srcset) {
  534. img.setAttribute('data-lazy-srcset', img.srcset);
  535. img.removeAttribute('srcset');
  536. }
  537.  
  538. // 添加到观察者
  539. if (this.observer) {
  540. this.observer.observe(img);
  541. }
  542.  
  543. this.processedImages.add(img);
  544. this.monitor.count('processed-images');
  545. return true;
  546. }
  547.  
  548. restoreImage(img) {
  549. const src = img.getAttribute('data-lazy-src');
  550. const srcset = img.getAttribute('data-lazy-srcset');
  551.  
  552. if (src) {
  553. img.src = src;
  554. img.removeAttribute('data-lazy-src');
  555. }
  556. if (srcset) {
  557. img.srcset = srcset;
  558. img.removeAttribute('data-lazy-srcset');
  559. }
  560.  
  561. this.processedImages.delete(img);
  562. }
  563.  
  564. batchProcess(images) {
  565. const batchSize = this.config.get('lazyLoad.batchSize');
  566. let processed = 0;
  567.  
  568. const processBatch = () => {
  569. const end = Math.min(processed + batchSize, images.length);
  570.  
  571. for (let i = processed; i < end; i++) {
  572. this.processImage(images[i]);
  573. }
  574.  
  575. processed = end;
  576.  
  577. if (processed < images.length) {
  578. (window.requestIdleCallback || window.requestAnimationFrame)(processBatch);
  579. } else {
  580. this.monitor.log(`懒加载处理完成,共处理 ${processed} 张图片`);
  581. }
  582. };
  583.  
  584. processBatch();
  585. }
  586.  
  587. processExistingImages() {
  588. const images = Array.from(document.querySelectorAll('img'));
  589. this.monitor.log(`发现 ${images.length} 张图片,开始批量处理`);
  590. this.batchProcess(images);
  591. }
  592.  
  593. // 改进的批处理调度 - 更好的并发控制
  594. scheduleBatchProcess() {
  595. if (this.batchScheduled || this.pendingImages.length === 0) return;
  596.  
  597. this.batchScheduled = true;
  598. (window.requestIdleCallback || window.requestAnimationFrame)(() => {
  599. const images = [...this.pendingImages];
  600. this.pendingImages = [];
  601. this.batchScheduled = false;
  602.  
  603. let processedCount = 0;
  604. images.forEach(img => {
  605. if (this.processImage(img)) {
  606. processedCount++;
  607. }
  608. });
  609.  
  610. if (processedCount > 0) {
  611. this.monitor.log(`动态处理 ${processedCount} 张新图片`);
  612. }
  613. });
  614. }
  615.  
  616. setupMutationObserver() {
  617. let pendingMutations = [];
  618. let processingScheduled = false;
  619.  
  620. const processMutations = () => {
  621. const mutations = [...pendingMutations];
  622. pendingMutations = [];
  623. processingScheduled = false;
  624.  
  625. mutations.forEach(mutation => {
  626. // 处理新增节点
  627. mutation.addedNodes.forEach(node => {
  628. if (isImageElement(node)) {
  629. this.pendingImages.push(node);
  630. } else if (node.querySelectorAll) {
  631. const images = node.querySelectorAll('img');
  632. this.pendingImages.push(...Array.from(images));
  633. }
  634. });
  635.  
  636. // 清理移除的节点
  637. mutation.removedNodes.forEach(node => {
  638. if (isImageElement(node) && this.observer) {
  639. this.observer.unobserve(node);
  640. this.processedImages.delete(node);
  641. this.processedElements.delete && this.processedElements.delete(node);
  642. }
  643. });
  644. });
  645.  
  646. this.scheduleBatchProcess();
  647. };
  648.  
  649. this.mutationObserver = new MutationObserver((mutations) => {
  650. pendingMutations.push(...mutations);
  651.  
  652. if (!processingScheduled) {
  653. processingScheduled = true;
  654. (window.requestIdleCallback || window.requestAnimationFrame)(processMutations);
  655. }
  656. });
  657.  
  658. this.mutationObserver.observe(document.body, {
  659. childList: true,
  660. subtree: true
  661. });
  662. }
  663.  
  664. destroy() {
  665. if (this.observer) {
  666. this.observer.disconnect();
  667. this.observer = null;
  668. }
  669. if (this.mutationObserver) {
  670. this.mutationObserver.disconnect();
  671. this.mutationObserver = null;
  672. }
  673.  
  674. // 恢复所有处理过的图片
  675. this.processedImages.forEach(img => this.restoreImage(img));
  676. this.processedImages.clear();
  677. }
  678. }
  679.  
  680. /**
  681. * 预加载管理器 - 改进异步处理
  682. */
  683. class PreloadManager {
  684. constructor(config, monitor) {
  685. this.config = config;
  686. this.monitor = monitor;
  687. this.preloaded = new LRUCache(this.config.get('preload.maxPreloads'));
  688. this.cssCache = new LRUCache(50);
  689. this.mutationObserver = null;
  690. }
  691.  
  692. async init() {
  693. if (!this.config.get('preload.enabled')) {
  694. this.monitor.log('预加载功能已禁用');
  695. return;
  696. }
  697.  
  698. if (this.config.isBlacklisted(location.hostname, 'preload')) {
  699. this.monitor.log('当前站点在预加载黑名单中');
  700. return;
  701. }
  702.  
  703. this.monitor.start('preload-init');
  704. await this.scanExistingResources(); // 改为异步
  705. this.setupMutationObserver();
  706. this.monitor.end('preload-init');
  707. }
  708.  
  709. getResourceType(url) {
  710. const ext = url.split('.').pop()?.toLowerCase();
  711. const types = this.config.get('preload.types');
  712.  
  713. if (!types.includes(ext)) return null;
  714.  
  715. switch (ext) {
  716. case 'css': return 'style';
  717. case 'js': return 'script';
  718. case 'woff':
  719. case 'woff2':
  720. case 'ttf':
  721. case 'otf': return 'font';
  722. default: return null;
  723. }
  724. }
  725.  
  726. doPreload(url, asType) {
  727. if (this.preloaded.has(url) || this.preloaded.size >= this.config.get('preload.maxPreloads')) {
  728. return false;
  729. }
  730.  
  731. try {
  732. const link = document.createElement('link');
  733. link.rel = 'preload';
  734. link.as = asType;
  735. link.href = url;
  736.  
  737. if (asType === 'font') {
  738. link.crossOrigin = 'anonymous';
  739. // 设置正确的 MIME 类型
  740. if (url.includes('.woff2')) link.type = 'font/woff2';
  741. else if (url.includes('.woff')) link.type = 'font/woff';
  742. else if (url.includes('.ttf')) link.type = 'font/ttf';
  743. else if (url.includes('.otf')) link.type = 'font/otf';
  744. }
  745.  
  746. document.head.appendChild(link);
  747. this.preloaded.set(url, true);
  748. this.monitor.log(`预加载 ${asType}: ${url}`);
  749. this.monitor.count('preloaded-resources');
  750. return true;
  751. } catch (e) {
  752. this.monitor.warn(`预加载失败: ${url}`, e);
  753. return false;
  754. }
  755. }
  756.  
  757. async extractFontsFromCSS(cssUrl) {
  758. if (this.cssCache.has(cssUrl)) {
  759. return this.cssCache.get(cssUrl);
  760. }
  761.  
  762. const operation = async () => {
  763. const controller = new AbortController();
  764. const timeoutId = setTimeout(() => controller.abort(), this.config.get('preload.fetchTimeout'));
  765.  
  766. try {
  767. const response = await fetch(cssUrl, {
  768. signal: controller.signal,
  769. mode: 'cors',
  770. credentials: 'omit'
  771. });
  772.  
  773. clearTimeout(timeoutId);
  774.  
  775. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  776.  
  777. const text = await response.text();
  778. const fontUrls = [];
  779. const fontRegex = /url\(["']?([^")']+\.(woff2?|ttf|otf))["']?\)/gi;
  780. let match;
  781.  
  782. while ((match = fontRegex.exec(text)) !== null) {
  783. const fontUrl = safeParseURL(match[1], cssUrl);
  784. if (fontUrl) {
  785. fontUrls.push(fontUrl.href);
  786. }
  787. }
  788.  
  789. this.cssCache.set(cssUrl, fontUrls);
  790. return fontUrls;
  791. } finally {
  792. clearTimeout(timeoutId);
  793. }
  794. };
  795.  
  796. try {
  797. // 使用重试机制
  798. return await RetryableOperation.executeWithRetry(
  799. operation,
  800. this.config.get('preload.retryAttempts')
  801. );
  802. } catch (e) {
  803. this.monitor.warn(`提取字体失败: ${cssUrl}`, e.message);
  804. this.cssCache.set(cssUrl, []);
  805. return [];
  806. }
  807. }
  808.  
  809. // 改进的异步资源扫描 - 更好的并发控制
  810. async scanExistingResources() {
  811. // 处理 CSS 文件
  812. const cssLinks = Array.from(document.querySelectorAll('link[rel="stylesheet"][href]'));
  813. const jsScripts = Array.from(document.querySelectorAll('script[src]'));
  814.  
  815. // 处理 CSS 文件的 Promise 数组
  816. const cssPromises = cssLinks.map(async link => {
  817. const cssUrl = link.href;
  818. const asType = this.getResourceType(cssUrl);
  819.  
  820. if (asType === 'style') {
  821. this.doPreload(cssUrl, asType);
  822.  
  823. // 异步提取和预加载字体
  824. try {
  825. const fontUrls = await this.extractFontsFromCSS(cssUrl);
  826. fontUrls.forEach(fontUrl => {
  827. const fontType = this.getResourceType(fontUrl);
  828. if (fontType === 'font') {
  829. this.doPreload(fontUrl, fontType);
  830. }
  831. });
  832. } catch (e) {
  833. // 忽略字体提取错误,不影响主流程
  834. this.monitor.warn(`CSS处理失败: ${cssUrl}`, e);
  835. }
  836. }
  837. });
  838.  
  839. // 处理 JS 文件
  840. jsScripts.forEach(script => {
  841. const asType = this.getResourceType(script.src);
  842. if (asType === 'script') {
  843. this.doPreload(script.src, asType);
  844. }
  845. });
  846.  
  847. // 等待所有 CSS 处理完成,但不阻塞初始化
  848. try {
  849. await Promise.allSettled(cssPromises);
  850. this.monitor.log(`资源扫描完成,处理了 ${cssLinks.length} CSS文件和 ${jsScripts.length} JS文件`);
  851. } catch (e) {
  852. this.monitor.warn('资源扫描过程中出现错误', e);
  853. }
  854. }
  855.  
  856. setupMutationObserver() {
  857. this.mutationObserver = new MutationObserver(debounce(async (mutations) => {
  858. const newCSSLinks = [];
  859. const newJSScripts = [];
  860.  
  861. mutations.forEach(mutation => {
  862. mutation.addedNodes.forEach(node => {
  863. if (node.tagName === 'LINK' && node.rel === 'stylesheet' && node.href) {
  864. newCSSLinks.push(node);
  865. } else if (node.tagName === 'SCRIPT' && node.src) {
  866. newJSScripts.push(node);
  867. }
  868. });
  869. });
  870.  
  871. // 异步处理新添加的资源
  872. if (newCSSLinks.length > 0 || newJSScripts.length > 0) {
  873. const promises = newCSSLinks.map(async node => {
  874. const asType = this.getResourceType(node.href);
  875. if (asType === 'style') {
  876. this.doPreload(node.href, asType);
  877.  
  878. // 异步处理字体
  879. try {
  880. const fontUrls = await this.extractFontsFromCSS(node.href);
  881. fontUrls.forEach(fontUrl => {
  882. const fontType = this.getResourceType(fontUrl);
  883. if (fontType === 'font') {
  884. this.doPreload(fontUrl, fontType);
  885. }
  886. });
  887. } catch (e) {
  888. // 忽略错误
  889. }
  890. }
  891. });
  892.  
  893. newJSScripts.forEach(node => {
  894. const asType = this.getResourceType(node.src);
  895. if (asType === 'script') {
  896. this.doPreload(node.src, asType);
  897. }
  898. });
  899.  
  900. // 不等待 Promise 完成,避免阻塞
  901. Promise.allSettled(promises).then(() => {
  902. this.monitor.log(`动态处理了 ${newCSSLinks.length} CSS ${newJSScripts.length} JS`);
  903. });
  904. }
  905. }, 200));
  906.  
  907. this.mutationObserver.observe(document.body, {
  908. childList: true,
  909. subtree: true
  910. });
  911. }
  912.  
  913. destroy() {
  914. if (this.mutationObserver) {
  915. this.mutationObserver.disconnect();
  916. this.mutationObserver = null;
  917. }
  918. this.preloaded.clear();
  919. this.cssCache.clear();
  920. }
  921. }
  922.  
  923. // 其他管理器类保持不变,仅引用已改进的配置和监控器
  924. class PreconnectManager {
  925. constructor(config, monitor) {
  926. this.config = config;
  927. this.monitor = monitor;
  928. this.connected = new LRUCache(this.config.get('preconnect.maxConnections'));
  929. this.mutationObserver = null;
  930. }
  931.  
  932. init() {
  933. if (!this.config.get('preconnect.enabled')) {
  934. this.monitor.log('预连接功能已禁用');
  935. return;
  936. }
  937.  
  938. if (this.config.isBlacklisted(location.hostname, 'preconnect')) {
  939. this.monitor.log('当前站点在预连接黑名单中');
  940. return;
  941. }
  942.  
  943. this.monitor.start('preconnect-init');
  944. this.scanExistingResources();
  945. this.setupMutationObserver();
  946. this.monitor.end('preconnect-init');
  947. }
  948.  
  949. shouldPreconnect(hostname) {
  950. if (!hostname || hostname === location.hostname) return false;
  951. if (this.connected.has(hostname)) return false;
  952.  
  953. const whitelist = this.config.get('preconnect.whitelist');
  954. return whitelist.some(domain => hostname.endsWith(domain));
  955. }
  956.  
  957. doPreconnect(hostname) {
  958. if (!this.shouldPreconnect(hostname)) return false;
  959.  
  960. try {
  961. const link = document.createElement('link');
  962. link.rel = 'preconnect';
  963. link.href = `https://${hostname}`;
  964. link.crossOrigin = 'anonymous';
  965. document.head.appendChild(link);
  966.  
  967. this.connected.set(hostname, true);
  968. this.monitor.log(`预连接: ${hostname}`);
  969. this.monitor.count('preconnected-domains');
  970. return true;
  971. } catch (e) {
  972. this.monitor.warn(`预连接失败: ${hostname}`, e);
  973. return false;
  974. }
  975. }
  976.  
  977. extractHostnames(elements) {
  978. const hostnames = new Set();
  979.  
  980. elements.forEach(el => {
  981. const url = safeParseURL(el.src || el.href);
  982. if (url && url.hostname !== location.hostname) {
  983. hostnames.add(url.hostname);
  984. }
  985. });
  986.  
  987. return Array.from(hostnames);
  988. }
  989.  
  990. scanExistingResources() {
  991. const selectors = [
  992. 'script[src]',
  993. 'link[href]',
  994. 'img[src]',
  995. 'audio[src]',
  996. 'video[src]',
  997. 'source[src]'
  998. ];
  999.  
  1000. const elements = document.querySelectorAll(selectors.join(','));
  1001. const hostnames = this.extractHostnames(elements);
  1002.  
  1003. let connected = 0;
  1004. hostnames.forEach(hostname => {
  1005. if (this.doPreconnect(hostname)) {
  1006. connected++;
  1007. }
  1008. });
  1009.  
  1010. this.monitor.log(`扫描完成,预连接 ${connected} 个域名`);
  1011. }
  1012.  
  1013. setupMutationObserver() {
  1014. this.mutationObserver = new MutationObserver(debounce((mutations) => {
  1015. const newElements = [];
  1016.  
  1017. mutations.forEach(mutation => {
  1018. mutation.addedNodes.forEach(node => {
  1019. if (node.src || node.href) {
  1020. newElements.push(node);
  1021. } else if (node.querySelectorAll) {
  1022. const elements = node.querySelectorAll('script[src], link[href], img[src]');
  1023. newElements.push(...elements);
  1024. }
  1025. });
  1026. });
  1027.  
  1028. if (newElements.length > 0) {
  1029. const hostnames = this.extractHostnames(newElements);
  1030. hostnames.forEach(hostname => this.doPreconnect(hostname));
  1031. }
  1032. }, 200));
  1033.  
  1034. this.mutationObserver.observe(document.body, {
  1035. childList: true,
  1036. subtree: true
  1037. });
  1038. }
  1039.  
  1040. destroy() {
  1041. if (this.mutationObserver) {
  1042. this.mutationObserver.disconnect();
  1043. this.mutationObserver = null;
  1044. }
  1045. this.connected.clear();
  1046. }
  1047. }
  1048.  
  1049. class LayoutStabilizer {
  1050. constructor(config, monitor) {
  1051. this.config = config;
  1052. this.monitor = monitor;
  1053. this.injectedStyle = null;
  1054. }
  1055.  
  1056. init() {
  1057. if (!this.config.get('layout.enabled')) {
  1058. this.monitor.log('布局优化功能已禁用');
  1059. return;
  1060. }
  1061.  
  1062. if (this.config.isBlacklisted(location.hostname, 'layout')) {
  1063. this.monitor.log('当前站点在布局优化黑名单中');
  1064. return;
  1065. }
  1066.  
  1067. this.monitor.start('layout-init');
  1068. this.injectStabilizationStyles();
  1069. this.monitor.end('layout-init');
  1070. }
  1071.  
  1072. generateCSS() {
  1073. const styles = [];
  1074.  
  1075. if (this.config.get('layout.stableImages')) {
  1076. styles.push(`
  1077. /* 图片布局稳定性优化 */
  1078. img:not([width]):not([height]):not([style*="width"]):not([style*="height"]) {
  1079. min-height: 1px;
  1080. // max-width: 100%;
  1081. // height: auto;
  1082. }
  1083.  
  1084. /* 现代浏览器的 aspect-ratio 支持 */
  1085. @supports (aspect-ratio: 1/1) {
  1086. img[width][height]:not([style*="aspect-ratio"]) {
  1087. aspect-ratio: attr(width) / attr(height);
  1088. }
  1089. }
  1090. `);
  1091. }
  1092.  
  1093. if (this.config.get('layout.stableIframes')) {
  1094. styles.push(`
  1095. /* iframe 布局稳定性优化 */
  1096. iframe:not([width]):not([height]):not([style*="width"]):not([style*="height"]) {
  1097. // width: 100%;
  1098. // height: auto;
  1099. min-height: 1px;
  1100. }
  1101.  
  1102. @supports (aspect-ratio: 16/9) {
  1103. iframe:not([style*="aspect-ratio"]) {
  1104. aspect-ratio: 16/9;
  1105. }
  1106. }
  1107. `);
  1108. }
  1109.  
  1110. return styles.join('\n');
  1111. }
  1112.  
  1113. injectStabilizationStyles() {
  1114. const css = this.generateCSS().trim();
  1115. if (!css) return;
  1116.  
  1117. try {
  1118. this.injectedStyle = document.createElement('style');
  1119. this.injectedStyle.setAttribute('data-optimizer', 'layout-stabilizer');
  1120. this.injectedStyle.textContent = css;
  1121.  
  1122. // 优先插入到 head,如果不存在则插入到 document
  1123. const target = document.head || document.documentElement;
  1124. target.appendChild(this.injectedStyle);
  1125.  
  1126. this.monitor.log('布局稳定性样式已注入');
  1127. } catch (e) {
  1128. this.monitor.warn('注入布局样式失败', e);
  1129. }
  1130. }
  1131.  
  1132. destroy() {
  1133. if (this.injectedStyle && this.injectedStyle.parentNode) {
  1134. this.injectedStyle.parentNode.removeChild(this.injectedStyle);
  1135. this.injectedStyle = null;
  1136. this.monitor.log('布局样式已移除');
  1137. }
  1138. }
  1139. }
  1140.  
  1141. // ========================
  1142. // 主优化器
  1143. // ========================
  1144.  
  1145. /**
  1146. * 主优化器类 - v2.0
  1147. */
  1148. class WebOptimizer {
  1149. constructor() {
  1150. this.config = new ConfigManager();
  1151. this.monitor = new PerformanceMonitor(this.config.get('debug'));
  1152.  
  1153. // 优化模块
  1154. this.modules = {
  1155. lazyLoad: new LazyLoadManager(this.config, this.monitor),
  1156. preconnect: new PreconnectManager(this.config, this.monitor),
  1157. preload: new PreloadManager(this.config, this.monitor),
  1158. layout: new LayoutStabilizer(this.config, this.monitor)
  1159. };
  1160.  
  1161. this.initialized = false;
  1162. this.cleanupTasks = [];
  1163. }
  1164.  
  1165. async init() {
  1166. if (this.initialized) return;
  1167.  
  1168. this.monitor.start('total-init');
  1169. this.monitor.log('Web Optimizer Enhanced v2.0 开始初始化');
  1170.  
  1171. // 检查全局黑名单
  1172. if (this.config.isBlacklisted(location.hostname)) {
  1173. this.monitor.log('当前站点在全局黑名单中,跳过所有优化');
  1174. return;
  1175. }
  1176.  
  1177. try {
  1178. // 等待 DOM 基本可用
  1179. if (document.readyState === 'loading') {
  1180. await new Promise(resolve => {
  1181. document.addEventListener('DOMContentLoaded', resolve, { once: true });
  1182. });
  1183. }
  1184.  
  1185. // 初始化各个模块 - 改进的错误隔离
  1186. const initPromises = Object.entries(this.modules).map(async ([name, module]) => {
  1187. try {
  1188. this.monitor.start(`init-${name}`);
  1189. await module.init();
  1190. this.monitor.end(`init-${name}`);
  1191. return { name, success: true };
  1192. } catch (e) {
  1193. this.monitor.error(`模块 ${name} 初始化失败`, e);
  1194. return { name, success: false, error: e };
  1195. }
  1196. });
  1197.  
  1198. const results = await Promise.allSettled(initPromises);
  1199. const successCount = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
  1200. this.monitor.log(`模块初始化完成,成功: ${successCount}/${Object.keys(this.modules).length}`);
  1201.  
  1202. // 设置清理任务
  1203. this.setupCleanupTasks();
  1204.  
  1205. this.initialized = true;
  1206. this.monitor.end('total-init');
  1207. this.monitor.log('Web Optimizer Enhanced v2.0 初始化完成');
  1208.  
  1209. // 显示初始化报告
  1210. this.showInitReport();
  1211.  
  1212. } catch (e) {
  1213. this.monitor.error('初始化失败', e);
  1214. }
  1215. }
  1216.  
  1217. setupCleanupTasks() {
  1218. // 定期清理缓存
  1219. const cleanupInterval = setInterval(() => {
  1220. Object.values(this.modules).forEach(module => {
  1221. if (module.cssCache) module.cssCache.clear();
  1222. if (module.connected) module.connected.clear();
  1223. if (module.preloaded) module.preloaded.clear();
  1224. });
  1225. this.monitor.log('定期清理完成');
  1226. }, 10 * 60 * 1000); // 10分钟
  1227.  
  1228. this.cleanupTasks.push(() => clearInterval(cleanupInterval));
  1229.  
  1230. // 页面卸载时清理
  1231. const cleanup = () => {
  1232. this.destroy();
  1233. };
  1234.  
  1235. window.addEventListener('beforeunload', cleanup);
  1236. this.cleanupTasks.push(() => {
  1237. window.removeEventListener('beforeunload', cleanup);
  1238. });
  1239. }
  1240.  
  1241. showInitReport() {
  1242. if (!this.config.get('debug')) return;
  1243.  
  1244. console.groupCollapsed('[Web Optimizer Enhanced v2.0] 初始化报告');
  1245. console.log('版本: 2.0');
  1246. console.log('当前域名:', location.hostname);
  1247. console.log('启用的功能:', Object.entries(this.config.config)
  1248. .filter(([key, value]) => typeof value === 'object' && value.enabled)
  1249. .map(([key]) => key)
  1250. );
  1251.  
  1252. // 显示性能计数器
  1253. console.log('性能计数:', {
  1254. 'processed-images': this.monitor.getCounter('processed-images'),
  1255. 'lazy-loaded-images': this.monitor.getCounter('lazy-loaded-images'),
  1256. 'preconnected-domains': this.monitor.getCounter('preconnected-domains'),
  1257. 'preloaded-resources': this.monitor.getCounter('preloaded-resources')
  1258. });
  1259.  
  1260. console.log('配置详情:', this.config.config);
  1261. console.groupEnd();
  1262. }
  1263.  
  1264. destroy() {
  1265. if (!this.initialized) return;
  1266.  
  1267. this.monitor.log('开始清理资源');
  1268.  
  1269. // 清理各个模块
  1270. Object.values(this.modules).forEach(module => {
  1271. if (module.destroy) {
  1272. try {
  1273. module.destroy();
  1274. } catch (e) {
  1275. this.monitor.warn('模块清理失败', e);
  1276. }
  1277. }
  1278. });
  1279.  
  1280. // 执行清理任务
  1281. this.cleanupTasks.forEach(task => {
  1282. try {
  1283. task();
  1284. } catch (e) {
  1285. this.monitor.warn('清理任务执行失败', e);
  1286. }
  1287. });
  1288. this.cleanupTasks = [];
  1289.  
  1290. this.initialized = false;
  1291. this.monitor.log('资源清理完成');
  1292. }
  1293.  
  1294. // 公共 API - 增强版
  1295. updateConfig(path, value) {
  1296. this.config.set(path, value);
  1297. this.monitor.log(`配置已更新: ${path} = ${value}`);
  1298.  
  1299. // 如果是调试模式变更,更新监控器
  1300. if (path === 'debug') {
  1301. this.monitor.debug = value;
  1302. }
  1303. }
  1304.  
  1305. getStats() {
  1306. return {
  1307. initialized: this.initialized,
  1308. version: '2.0',
  1309. hostname: location.hostname,
  1310. config: this.config.config,
  1311. modules: Object.keys(this.modules),
  1312. counters: {
  1313. 'processed-images': this.monitor.getCounter('processed-images'),
  1314. 'lazy-loaded-images': this.monitor.getCounter('lazy-loaded-images'),
  1315. 'preconnected-domains': this.monitor.getCounter('preconnected-domains'),
  1316. 'preloaded-resources': this.monitor.getCounter('preloaded-resources')
  1317. }
  1318. };
  1319. }
  1320.  
  1321. // 新增:性能报告
  1322. getPerformanceReport() {
  1323. const stats = this.getStats();
  1324. const imageStats = {
  1325. total: document.querySelectorAll('img').length,
  1326. processed: stats.counters['processed-images'],
  1327. lazyLoaded: stats.counters['lazy-loaded-images']
  1328. };
  1329.  
  1330. return {
  1331. ...stats,
  1332. performance: {
  1333. images: imageStats,
  1334. domains: stats.counters['preconnected-domains'],
  1335. resources: stats.counters['preloaded-resources'],
  1336. efficiency: imageStats.total > 0 ? (imageStats.processed / imageStats.total * 100).toFixed(1) + '%' : '0%'
  1337. }
  1338. };
  1339. }
  1340. }
  1341.  
  1342. // ========================
  1343. // 全局初始化
  1344. // ========================
  1345.  
  1346. // 创建全局实例
  1347. const optimizer = new WebOptimizer();
  1348.  
  1349. // 暴露到全局作用域(调试用)
  1350. if (typeof window !== 'undefined') {
  1351. window.WebOptimizer = optimizer;
  1352.  
  1353. // v2.0 新增:暴露工具函数供调试使用
  1354. window.WebOptimizerUtils = {
  1355. debounce,
  1356. throttle,
  1357. safeParseURL,
  1358. isElementVisible,
  1359. deepMerge,
  1360. delay,
  1361. RetryableOperation
  1362. };
  1363. }
  1364.  
  1365. // 启动优化器
  1366. if (document.readyState === 'loading') {
  1367. document.addEventListener('DOMContentLoaded', () => optimizer.init());
  1368. } else {
  1369. // 延迟一点时间,确保页面基本稳定
  1370. setTimeout(() => optimizer.init(), 100);
  1371. }
  1372.  
  1373. })();