loading

图片预加载,可以提前加载比较大的图片,或者gif图,防止白屏

  1. function loadImage_gif(url, callback) {
  2. var img = new Image(); //创建一个Image对象,实现图片的预下载
  3. img.onload = function () {
  4. img.onload = null;
  5. callback(img);//回掉函数
  6. };
  7. img.src = url;
  8. }
  9. useEffect(() => {
  10. const urlList = [gif, bg1, bg2, bg3, bg4];//react中引入的图片
  11. loadImage(urlList, (img) => {
  12. console.log(urlList, 'imgUrlList');
  13. });
  14. }, []);

对于一个使用大量图片的web App,图片预加载的使用很有必要了

  1. // data:
  2. data(){
  3. // 预加载
  4. preloadAudio: {},
  5. preloadImg1: {},
  6. preloadImg2: {},
  7. preloadImg3: {},
  8. preloadImg4: {},
  9. },
  10. // 钩子函数
  11. mounted() {
  12. //图片预加载-缓存单词、图片音频
  13. this.preloadAudio = new Audio();
  14. this.preloadImg1 = new Image();
  15. this.preloadImg2 = new Image();
  16. this.preloadImg3 = new Image();
  17. this.preloadImg4 = new Image();
  18. },
  19. // methods;
  20. /**
  21. * 图片预处理,只发请求
  22. * @Date: 2019年11月4日15:26:11
  23. * @author
  24. */
  25. searchWordInfoByWordIdPre() {
  26. var vm = this; // 保存原有对象
  27. //判断是否是最后一个单词
  28. // console.log(1);
  29. // console.log(vm.wordIdList);
  30. vm.$axios.get(swipeLeft + "?reviewFlag=" + vm.reviewFlag).then(res => {
  31. if (res.data.code === "0000") {
  32. //储存单词和id
  33. //将所有图片填充到数组-暂时写死
  34. vm.wordListPre = [];
  35. vm.wordListPre.push(res.data.data.wordPicture1);
  36. vm.wordListPre.push(res.data.data.wordPicture2);
  37. vm.wordListPre.push(res.data.data.wordPicture3);
  38. vm.wordListPre.push(res.data.data.wordPicture4);
  39. vm.wordListPre.push(res.data.data.audio);
  40. vm.wordListPre.push(res.data.data.word);
  41. vm.wordListPre.push(res.data.data.id);
  42. vm.wordIdList.push(res.data.data.id);
  43. vm.preloadImg();
  44. }
  45. //GOTO
  46. if (res.data.code === "2222") {
  47. this.$toast("任务已完成,即将跳转单词检测");
  48. this.isTurnSuccess = true;
  49. }
  50. });
  51. },
  52. /**
  53. * 预加载
  54. * @Date: 2019年11月4日15:26:111
  55. * @author 苑仁杰
  56. */
  57. preloadImg() {
  58. // 单词资源实例化
  59. var vm = this;
  60. vm.preloadImg1.src = vm.wordListPre[0];
  61. vm.preloadImg2.src = vm.wordListPre[1];
  62. vm.preloadImg3.src = vm.wordListPre[2];
  63. vm.preloadImg4.src = vm.wordListPre[3];
  64. vm.preloadAudio.src = vm.wordListPre[4];
  65. },

骨架屏

骨架屏就是在页面数据尚未加载前先给用户展示出页面的大致结构,直到请求数据返回后再渲染页面,补充进需要显示的数据内容。常用于文章列表、动态列表页等相对比较规则的列表页面。
很多项目中都有应用:ex:饿了么h5版本,知乎,facebook等网站中都有应用。

两类用途

简介中作了关于用途的说明,但是仍然可以继续细分:

  1. 作为spa中路由切换的loading,结合组件的生命周期和ajax请求返回的时机来使用.
  2. 作为首屏渲染的优化.

    第一类用途

    第一类用途需要自己编写骨架屏,推荐两个成熟方便定制的svg组件定制为骨架屏的方案
  1. cssUnit的配置: 需要使用自适应的单位,按照文档给出的选择范围选,直接用 px 生成的比例会不合适
  2. puppeteer有大概80M, 安装的时候有可能不能一次下载成功.
  • 原理:

通过 puppeteer 在服务端操控 headless Chrome 打开开发中的需要生成骨架屏的页面,在等待页面加载渲染完成之后,在保留页面布局样式的前提下,通过对页面中元素进行删减或增添,对已有元素通过层叠样式进行覆盖,这样达到在不改变页面布局下,隐藏图片和文字,通过样式覆盖,使得其展示为灰色块。然后将修改后的 HTML 和 CSS 样式提取出来,这样就是骨架屏了.

其他方案

结合ssr render/prerender来使用:

  1. 事先编写好骨架屏组件通过ssr render 解析注入html文件中(除了需要自己编写外其实过程类似于上面的自动化方案)参考文章
  2. 1中事先编写好的骨架屏组件可以用图片代替 (svg) ;或者设计师设计好.

    小程序的骨架屏

  3. 不存在预渲染的概念,但是还是可以通过自己预先编写骨架屏组件放在页面中,等到异步请求的数据回来后更新页面.

    web worker

    场景

    本文将分享一个web Worker在实际项目中使用的场景,在我们项目中,我们把从查询接口获取到的数据,格式化的过程,放在web worker中进行。
    首先简单介绍下我们的项目,可视化相关项目,接口主要分为两大方面,配置查询和数据查询接口,其中数据查询接口是影响页面性能指标的一个很关键的因素,并且从查询接口获取到数据之后的格式化过程逻辑,全部放在fe中了,但是我们知道,数据格式化这个过程可能会涉及一些比较大的计算,因此我们选择将它拆出来放在一个单独web worker线程中,减少对主线程造成影响。

    实现

    具体解释看注释,写的比较详细 ```javascript // workerHelper.ts import Worker from ‘worker-loader?inline=no-fallback!./query.worker.ts’; import axios, { CancelTokenSource } from ‘axios’; import { ErrorType } from ‘@/api’; const { CancelToken } = axios;

type EventHanler = { resolve: (value: any) => void; reject: (reason?: any) => void; }; export enum ErrorType { TIMEOUT, REQUEST, CANCEL }

const requestMap = new Map();

// 调用接口 const query = async ( params, ) => { let resultData; await axios.post(******, params, { headers: {}, cancelToken: cancelToken.token, }) .then(res => { if (res.status === 200 && res.data.code === 0) { resultData = res.data.data; } else if (res.status === 404) { throw ‘request 404’; } else { throw { message: res.data.msg || res.statusText || ‘’, logId, }; } }) .catch(err => { // 不是手动取消 const errType = err.CANCEL ? ErrorType.CANCEL : (err.message ?? err)?.includes(‘timeout’) ? ErrorType.TIMEOUT : ErrorType.REQUEST; const message = errType === ErrorType.TIMEOUT ? ‘request timeout’ : err.message ?? err; const logId = err.logId ?? ‘’; throw { message, type: errType, logId, }; }); return resultData; };

export default class WorkerHelper { private static _worker: Worker | null; // 定义一个worker // 初始化 static init() { if (!this._worker) { // 注册一个数据格式化worker this._worker = new Worker(); // webworker异常 this._worker.addEventListener(‘error’, e => { throw e; }); } }

// 查询函数,对外提供 static query( params: any, ): Promise { return new Promise((resolve, reject) => { // 如果未初始化,初始化 this.init(); query(params) .then(data => { // 查询到数据之后给worker发送消息启动格式化数据线程 this._worker?.postMessage({ method: ‘format’, options: { id, data, type, params, lang: getCurrentLang() }, }); }) .catch((e: Error) => { }); }); }

/*

  • @param id 组件id */ static cancelQuery(id: string) { requestMap.get(id as string)?.cancel(); }

    /*

  • @param 批量取消 */ static cancelMoreQuery(ids: string[]) { for(let id of ids){ requestMap.get(id as string)?.cancel(); }

    } // 终止worker static terminate() { this._worker?.terminate(); this._worker = null; } }

  1. ```javascript
  2. // 具体的worker query.worker.ts
  3. interface QueryParams {
  4. params: any;
  5. }
  6. interface CancelParams {
  7. id: string;
  8. }
  9. const ctx: Worker = self as any;
  10. const formatHandler = async options => {
  11. const { data, id, type, params, lang } = options;
  12. let result, error;
  13. try {
  14. if (!id) {
  15. error = new Error('id 不存在');
  16. throw error;
  17. }
  18. ...
  19. result = forMate(data)
  20. } catch (e) {
  21. error = e;
  22. } finally {
  23. ctx.postMessage({ id, data: result, error });
  24. }
  25. };
  26. ctx.addEventListener(
  27. 'message',
  28. (
  29. event: MessageEvent<{
  30. method: 'format' | 'cancelQuery';
  31. options: QueryParams | CancelParams;
  32. }>
  33. ) => {
  34. const {
  35. data: { method, options },
  36. } = event;
  37. if (method === 'format') {
  38. // 格式化数据
  39. formatHandler(options);
  40. }
  41. }
  42. );

使用

  1. // 具体使用的业务方
  2. import WorkerHelper from '@/utils/workerHelper';
  3. ...
  4. const queryData = async (params) => {
  5. isLoading.value = true;
  6. const { data, series, delayedMessage, feQueryConfig, percent: percentConfig } = await WorkerHelper.query(
  7. params,
  8. );
  9. ...
  10. };
  11. ...

虚拟列表

虚拟列表是什么?

简单来说,虚拟列表值得就是【可视区域渲染】的列表。
在有大量数据需要展示在页面上时,通过监听用户的滚动事件,动态地只对可见区域进行渲染,对非可见区域不渲染,从而达到极高的渲染性能。
首先弄清楚两个概念,【可滚动区域】和【可见区域】。
可滚动区域:「 整个列表的数据量每项列表的高度 」真实列表长度这部分区域。即需要展示的数据区域。
假设有 1000 条数据,每个列表项的高度是 30,那么可滚动的区域的高度就是 1000
30。当用户改变列表的滚动条的当前滚动值的时候,会造成可见区域的内容的变更。
可见区域:设定的列表的高度,用户可见的,需要动态渲染的区域。

用vue实现一个简单的虚拟列表

虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
整体代码部分

  1. <template>
  2. <div>
  3. <h1>虚拟列表学习</h1>
  4. <!-- 使用虚拟列表 -->
  5. <!-- 可视区域容器 监听scroll事件 -->
  6. <div class="virtual" @scroll="scroll" :style="{ height: 400 + 'px' }">
  7. <div class="list" :style="{ height: dataLengh * itemHeight + 'px' }">
  8. <!-- 列表项的渲染区域 margin-top定位当前滚动的位置-->
  9. <ul :style="{ 'margin-top': `${scrollTop}px` }">
  10. <li
  11. v-for="(item, index) in visiablelist"
  12. :key="index"
  13. :style="{ height: itemHeight + 'px' }"
  14. >
  15. {{ item }}
  16. </li>
  17. </ul>
  18. </div>
  19. </div>
  20. </div>
  21. </template>
  22. <script>
  23. import { reactive, toRefs, computed } from "vue";
  24. export default {
  25. setup() {
  26. const dataLengh = 10000; // 列表数据量
  27. const itemHeight = 40; // 列表每项高度
  28. const data = reactive({
  29. scrollTop: 0, // 当前滚动的位置
  30. startIndex: 0, // 数据的起始索引
  31. endIndex: 10, //数据的结束索引
  32. });
  33. function scroll(e) {
  34. // 获取当前滚动的位置
  35. const scrollTop = e.target.scrollTop;
  36. data.scrollTop = scrollTop;
  37. // 此时开始的索引
  38. data.startIndex = Math.floor(scrollTop / itemHeight);
  39. // 此时结束的索引
  40. data.endIndex = data.startIndex + 10;
  41. }
  42. // 创建一个长数组
  43. const list = new Array(dataLengh);
  44. for (var i = 0; i < list.length; i++) {
  45. list[i] = i ;
  46. }
  47. // 可视列表部分 从已有数组中返回选定的元素,返回一个子数组,即需要展示部分
  48. const visiablelist = computed(() => {
  49. return list.slice(data.startIndex, data.endIndex);
  50. });
  51. return {
  52. ...toRefs(data),
  53. scroll,
  54. visiablelist,
  55. dataLengh,
  56. itemHeight,
  57. list
  58. };
  59. },
  60. };
  61. </script>
  62. <style scoped>
  63. .virtual {
  64. border: solid 1px #eee;
  65. height: 400px;
  66. overflow:auto;
  67. }
  68. .list {
  69. overflow: hidden;
  70. }
  71. ul {
  72. list-style: none;
  73. padding: 0;
  74. margin: 0;
  75. }
  76. li {
  77. outline: solid 1px #fff;
  78. }
  79. </style>

参考链接

再谈前端虚拟列表的实现:https://zhuanlan.zhihu.com/p/34585166.
「前端进阶」高性能渲染十万条数据(虚拟列表):https://juejin.cn/post/6844903982742110216#heading-3

懒加载

一懒加载

1.什么是懒加载
懒加载也就是延迟加载。
当访问一个页面的时候,先把img元素或是其他元素的背景图片路径替换成一张大小为1*1px图片的路径(这样就只需请求一次,俗称占位图),只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,让图片显示出来。这就是图片懒加载。
2.为什么要使用懒加载?
很多页面,内容很丰富,页面很长,图片较多。比如说各种商城页面。这些页面图片数量多,而且比较大,少说百来K,多则上兆。要是页面载入就一次性加载完毕。估计大家都会等到黄花变成黄花菜了。
3.懒加载的原理是什么?
页面中的img元素,如果没有src属性,浏览器就不会发出请求去下载图片,只有通过javascript设置了图片路径,浏览器才会发送请求。
懒加载的原理就是先在页面中把所有的图片统一使用一张占位图进行占位,把正真的路径存在元素的“data-url”(这个名字起个自己认识好记的就行)属性里,要用的时候就取出来,再设置;
4.懒加载的实现步骤?
1)首先,不要将图片地址放到src属性中,而是放到其它属性(data-original)中。
2)页面加载完成后,根据scrollTop判断图片是否在用户的视野内,如果在,则将data-original属性中的值取出存放到src属性中。
3)在滚动事件中重复判断图片是否进入视野,如果进入,则将data-original属性中的值取出存放到src属性中。
5.懒加载的优点是什么?
页面加载速度快、可以减轻服务器的压力、节约了流量,用户体验好

二预加载

1.什么是预加载?
提前加载图片,当用户需要查看时可直接从本地缓存中渲染
2.为什么要使用预加载?
图片预先加载到浏览器中,访问者便可顺利地在你的网站上冲浪,并享受到极快的加载速度。这对图片画廊及图片占据很大比例的网站来说十分有利,它保证了图片快速、无缝地发布,也可帮助用户在浏览你网站内容时获得更好的用户体验。
3.实现预加载的方法有哪些?
方法一:用CSS和JavaScript实现预加载
方法二:仅使用JavaScript实现预加载
方法三:使用Ajax实现预加载
详见:http://web.jobbole.com/86785/


三懒加载和预加载的对比

1)概念:
懒加载也叫延迟加载:JS图片延迟加载,延迟加载图片或符合某些条件时才加载某些图片。
预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
2)区别:
两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
3)懒加载的意义及实现方式有:
意义:
懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。
实现方式:
1.第一种是纯粹的延迟加载,使用setTimeOut或setInterval进行加载延迟.
2.第二种是条件加载,符合某些条件,或触发了某些事件才开始异步下载。
3.第三种是可视区加载,即仅加载用户可以看到的区域,这个主要由监控滚动条来实现,一般会在距用户看到某图片前一定距离遍开始加载,这样能保证用户拉下时正好能看到图片。
4)预加载的意义及实现方式有:
意义:
预加载可以说是牺牲服务器前端性能,换取更好的用户体验,这样可以使用户的操作得到最快的反映。
实现方式:
实现预载的方法非常多,比如:用CSS和JavaScript实现预加载;仅使用JavaScript实现预加载;使用Ajax实现预加载。
常用的是new Image();设置其src来实现预载,再使用onload方法回调预载完成事件。只要浏览器把图片下载到本地,同样的src就会使用缓存,这是最基本也是最实用的预载方法。当Image下载完图片头后,会得到宽和高,因此可以在预载前得到图片的大小(方法是用记时器轮循宽高变化)。

四判断是否在可视区内的代码

  1. // 是否在页面可视区内
  2. function isInVisibleArea(el) {
  3. var rect = el.getBoundingClientRect();
  4. return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
  5. }

屏幕可视窗口大小

  1. 原生方法:
  2. window.innerHeight 标准浏览器及IE9+ ||
  3. document.documentElement.clientHeight 标准浏览器及低版本IE标准模式 ||
  4. document.body.clientHeight 低版本混杂模式
  5. jQuery方法:
  6. $(window).height();

浏览器窗口顶部与文档顶部之间的距离,也就是滚动条滚动的距离:

  1. 原生方法:
  2. window.pagYoffset 标准浏览器及IE9+ ||
  3. document.documentElement.scrollTop 兼容ie低版本的标准模式 ||
  4. document.body.scrollTop 兼容混杂模式;
  5. jQuery方法:
  6. $(document).scrollTop();

实列分析

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
  6. <title>4.3 资源按需加载和预加载</title>
  7. <link rel="stylesheet" href="css/base.css">
  8. <link rel="stylesheet" href="css/icons.css">
  9. <link rel="stylesheet" href="css/index.css">
  10. <script src="js/flexible.js"></script>
  11. </head>
  12. <body>
  13. <header class="header-container">
  14. <div class="navbar">
  15. <div class="navbar-left">
  16. <i class="iconfont icon-scan"></i>
  17. </div>
  18. <div class="navbar-center">
  19. <div class="searchBox">
  20. <div class="searchBox-prepend">
  21. <i class="iconfont icon-search"></i>
  22. </div>
  23. <input type="text" placeholder="开学季有礼,好货5折起" class="searchBox-input">
  24. <div class="searchBox-append">
  25. <i class="iconfont icon-close"></i>
  26. </div>
  27. </div>
  28. </div>
  29. <div class="navbar-right">
  30. <i class="iconfont icon-msg"></i>
  31. </div>
  32. </div>
  33. </header>
  34. <div class="main-container">
  35. <div class="slider-container">
  36. <img src="img/slider/1.jpg" alt="slider">
  37. </div>
  38. <nav class="nav-container">
  39. <ul class="nav">
  40. <li class="nav-item">
  41. <a href="###" class="nav-link">
  42. <img src="img/nav/1.png" alt="nav" class="nav-img">
  43. <span class="nav-text">团购</span>
  44. </a>
  45. </li>
  46. <li class="nav-item">
  47. <a href="###" class="nav-link">
  48. <img src="img/nav/2.png" alt="nav" class="nav-img">
  49. <span class="nav-text">一元购</span>
  50. </a>
  51. </li>
  52. <li class="nav-item">
  53. <a href="###" class="nav-link">
  54. <img src="img/nav/3.png" alt="nav" class="nav-img">
  55. <span class="nav-text">优惠券</span>
  56. </a>
  57. </li>
  58. <li class="nav-item">
  59. <a href="###" class="nav-link">
  60. <img src="img/nav/4.png" alt="nav" class="nav-img">
  61. <span class="nav-text">教育</span>
  62. </a>
  63. </li>
  64. <li class="nav-item">
  65. <a href="###" class="nav-link">
  66. <img src="img/nav/5.png" alt="nav" class="nav-img">
  67. <span class="nav-text">旅行</span>
  68. </a>
  69. </li>
  70. <li class="nav-item">
  71. <a href="###" class="nav-link">
  72. <img src="img/nav/6.png" alt="nav" class="nav-img">
  73. <span class="nav-text">在线订餐</span>
  74. </a>
  75. </li>
  76. <li class="nav-item">
  77. <a href="###" class="nav-link">
  78. <img src="img/nav/7.png" alt="nav" class="nav-img">
  79. <span class="nav-text">庆典</span>
  80. </a>
  81. </li>
  82. <li class="nav-item">
  83. <a href="###" class="nav-link">
  84. <img src="img/nav/8.png" alt="nav" class="nav-img">
  85. <span class="nav-text">秒杀</span>
  86. </a>
  87. </li>
  88. <li class="nav-item">
  89. <a href="###" class="nav-link">
  90. <img src="img/nav/9.png" alt="nav" class="nav-img">
  91. <span class="nav-text">拍卖</span>
  92. </a>
  93. </li>
  94. <li class="nav-item">
  95. <a href="###" class="nav-link">
  96. <img src="img/nav/10.png" alt="nav" class="nav-img">
  97. <span class="nav-text">服务</span>
  98. </a>
  99. </li>
  100. </ul>
  101. </nav>
  102. <div class="recommend-container">
  103. <ul class="recommend">
  104. <li class="recommend-item">
  105. <a href="###" class="recommend-link">
  106. <p class="recommend-pic">
  107. <img src="img/loading.gif" data-src="img/recommend/1.jpg" alt="recommend" class="recommend-img lazyload-img">
  108. </p>
  109. <p class="recommend-name">欧派整体橱柜定制简约现代</p>
  110. <p class="recommend-origPrice">
  111. <del2000.00</del>
  112. </p>
  113. <p class="recommend-info">
  114. <span class="recommend-price">¥<strong class="recommend-price-num">1000</strong></span>
  115. <span class="recommend-count">985件已售</span>
  116. </p>
  117. </a>
  118. </li>
  119. <li class="recommend-item">
  120. <a href="###" class="recommend-link">
  121. <p class="recommend-pic">
  122. <img src="img/loading.gif" data-src="img/recommend/2.jpg" alt="recommend" class="recommend-img lazyload-img">
  123. </p>
  124. <p class="recommend-name">创维554K超高清HDR</p>
  125. <p class="recommend-origPrice">
  126. <del2999.00</del>
  127. </p>
  128. <p class="recommend-info">
  129. <span class="recommend-price">¥<strong class="recommend-price-num">2299</strong></span>
  130. <span class="recommend-count">63件已售</span>
  131. </p>
  132. </a>
  133. </li>
  134. <li class="recommend-item">
  135. <a href="###" class="recommend-link">
  136. <p class="recommend-pic">
  137. <img src="img/loading.gif" data-src="img/recommend/3.jpg" alt="recommend" class="recommend-img lazyload-img">
  138. </p>
  139. <p class="recommend-name">【到手259元】苏泊尔 5L电压力锅</p>
  140. <p class="recommend-origPrice">
  141. <del799.00</del>
  142. </p>
  143. <p class="recommend-info">
  144. <span class="recommend-price">¥<strong class="recommend-price-num">299</strong></span>
  145. <span class="recommend-count">1908件已售</span>
  146. </p>
  147. </a>
  148. </li>
  149. <li class="recommend-item">
  150. <a href="###" class="recommend-link">
  151. <p class="recommend-pic">
  152. <img src="img/loading.gif" data-src="img/recommend/4.jpg" alt="recommend" class="recommend-img lazyload-img">
  153. </p>
  154. <p class="recommend-name">三只松鼠坚果礼包</p>
  155. <p class="recommend-origPrice">
  156. <del125.00</del>
  157. </p>
  158. <p class="recommend-info">
  159. <span class="recommend-price">¥<strong class="recommend-price-num">108</strong></span>
  160. <span class="recommend-count">9532件已售</span>
  161. </p>
  162. </a>
  163. </li>
  164. <li class="recommend-item">
  165. <a href="###" class="recommend-link">
  166. <p class="recommend-pic">
  167. <img src="img/loading.gif" data-src="img/recommend/5.jpg" alt="recommend" class="recommend-img lazyload-img">
  168. </p>
  169. <p class="recommend-name">蓝月亮洗衣液12斤</p>
  170. <p class="recommend-origPrice">
  171. <del133.40</del>
  172. </p>
  173. <p class="recommend-info">
  174. <span class="recommend-price">¥<strong class="recommend-price-num">89.9</strong></span>
  175. <span class="recommend-count">5399件已售</span>
  176. </p>
  177. </a>
  178. </li>
  179. <li class="recommend-item">
  180. <a href="###" class="recommend-link">
  181. <p class="recommend-pic">
  182. <img src="img/loading.gif" data-src="img/recommend/6.jpg" alt="recommend" class="recommend-img lazyload-img">
  183. </p>
  184. <p class="recommend-name">福临门葵花玉米油</p>
  185. <p class="recommend-origPrice">
  186. <del109.90</del>
  187. </p>
  188. <p class="recommend-info">
  189. <span class="recommend-price">¥<strong class="recommend-price-num">89.9</strong></span>
  190. <span class="recommend-count">6294件已售</span>
  191. </p>
  192. </a>
  193. </li>
  194. <li class="recommend-item">
  195. <a href="###" class="recommend-link">
  196. <p class="recommend-pic">
  197. <img src="img/loading.gif" data-src="img/recommend/7.jpg" alt="recommend" class="recommend-img lazyload-img">
  198. </p>
  199. <p class="recommend-name">TP-LINK 全千兆端口双频无线路由器</p>
  200. <p class="recommend-origPrice">
  201. <del179.00</del>
  202. </p>
  203. <p class="recommend-info">
  204. <span class="recommend-price">¥<strong class="recommend-price-num">169</strong></span>
  205. <span class="recommend-count">4255件已售</span>
  206. </p>
  207. </a>
  208. </li>
  209. <li class="recommend-item">
  210. <a href="###" class="recommend-link">
  211. <p class="recommend-pic">
  212. <img src="img/loading.gif" data-src="img/recommend/8.jpg" alt="recommend" class="recommend-img lazyload-img">
  213. </p>
  214. <p class="recommend-name">【前1800名再减50】家用高压洗车机</p>
  215. <p class="recommend-origPrice">
  216. <del790.00</del>
  217. </p>
  218. <p class="recommend-info">
  219. <span class="recommend-price">¥<strong class="recommend-price-num">268</strong></span>
  220. <span class="recommend-count">1599件已售</span>
  221. </p>
  222. </a>
  223. </li>
  224. <li class="recommend-item">
  225. <a href="###" class="recommend-link">
  226. <p class="recommend-pic">
  227. <img src="img/loading.gif" data-src="img/recommend/9.jpg" alt="recommend" class="recommend-img lazyload-img">
  228. </p>
  229. <p class="recommend-name">德国鲁茜rusch迷你婴儿辅食机 宝宝</p>
  230. <p class="recommend-origPrice">
  231. <del898.00</del>
  232. </p>
  233. <p class="recommend-info">
  234. <span class="recommend-price">¥<strong class="recommend-price-num">159</strong></span>
  235. <span class="recommend-count">881件已售</span>
  236. </p>
  237. </a>
  238. </li>
  239. <li class="recommend-item">
  240. <a href="###" class="recommend-link">
  241. <p class="recommend-pic">
  242. <img src="img/loading.gif" data-src="img/recommend/10.jpg" alt="recommend" class="recommend-img lazyload-img">
  243. </p>
  244. <p class="recommend-name">西域之尚红枣500g*5袋</p>
  245. <p class="recommend-origPrice">
  246. <del89.00</del>
  247. </p>
  248. <p class="recommend-info">
  249. <span class="recommend-price">¥<strong class="recommend-price-num">29.9</strong></span>
  250. <span class="recommend-count">15049件已售</span>
  251. </p>
  252. </a>
  253. </li>
  254. </ul>
  255. </div>
  256. <!-- 资源按需加载 -->
  257. <div id="product" class="product" style="height: 500px; background-color: red; text-align: center; line-height: 500px; font-size: 2.5rem; color: #fff;">
  258. <img src="img/loading.gif" alt="">
  259. </div>
  260. </div>
  261. <div class="tabbar-container">
  262. <ul class="tabbar">
  263. <li class="tabbar-item tabbar-item-active">
  264. <a href="###" class="tabbar-link">
  265. <i class="iconfont icon-home"></i>
  266. <span>首页</span>
  267. </a>
  268. </li>
  269. <li class="tabbar-item">
  270. <a href="###" class="tabbar-link">
  271. <i class="iconfont icon-category"></i>
  272. <span>分类页</span>
  273. </a>
  274. </li>
  275. <li class="tabbar-item">
  276. <a href="###" class="tabbar-link">
  277. <i class="iconfont icon-cart"></i>
  278. <span>购物车</span>
  279. </a>
  280. </li>
  281. <li class="tabbar-item">
  282. <a href="###" class="tabbar-link">
  283. <i class="iconfont icon-personal"></i>
  284. <span>个人中心</span>
  285. </a>
  286. </li>
  287. </ul>
  288. </div>
  289. <script>
  290. // 1. 图片的按需加载
  291. var lazyLoadClass = '.lazyload-img';
  292. var imgArr = Array.prototype.slice.call(document.querySelectorAll(lazyLoadClass));
  293. // console.log(imgArr);
  294. // var tmpArr = [];
  295. // for () {
  296. // tmpArr.push();
  297. // }
  298. lazyLoadImgs();
  299. var timer = null;
  300. window.addEventListener('scroll', function () {
  301. clearTimeout(timer);
  302. timer = setTimeout(function () {
  303. lazyLoadImgs();
  304. }, 100);
  305. }, false);
  306. function lazyLoadImgs() {
  307. for (var i = 0; i < imgArr.length; i++) {
  308. if (isInVisibleArea(imgArr[i])) {
  309. imgArr[i].src = imgArr[i].getAttribute('data-src');
  310. imgArr.splice(i, 1);
  311. i--;
  312. // [1, 3]
  313. // i = 1
  314. // i--; i = 0
  315. // i = 1
  316. }
  317. }
  318. }
  319. // 是否在页面可视区内
  320. function isInVisibleArea(el) {
  321. var rect = el.getBoundingClientRect();
  322. return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
  323. }
  324. // 2. 其他内容的按需加载
  325. loadProduct();
  326. window.addEventListener('scroll', loadProduct, false);
  327. function loadProduct() {
  328. if (isInVisibleArea(document.getElementById('product'))) {
  329. var script = document.createElement('script');
  330. // script.src = 'js/loadProduct.js';
  331. setTimeout(function () {
  332. script.src = 'js/loadProduct.js';
  333. }, 1000);
  334. document.body.appendChild(script);
  335. window.removeEventListener('scroll', loadProduct, false);
  336. }
  337. }
  338. // 3. 图片预加载
  339. var img = new Image();
  340. img.src = 'img/recommend/5.jpg';
  341. </script>
  342. </body>
  343. </html>

dom/style批量更新

如下的这些DOM操作会导致重绘或重排:

  • 增加、删除和修改可见DOM元素
  • 页面初始化的渲染
  • 移动DOM元素
  • 修改CSS样式,改变DOM元素的尺寸
  • DOM元素内容改变,使得尺寸被撑大
  • 浏览器窗口尺寸改变
  • 浏览器窗口滚动

    1. 合并多次的DOM操作为单次的DOM操作

    最常见频繁进行DOM操作的是频繁修改DOM元素的样式,代码类似如下:
    1. element.style.borderColor = '#f00';
    2. element.style.borderStyle = 'solid';
    3. element.style.borderWidth = '1px';
    这种编码方式会因为频繁更改DOM元素的样式,触发页面多次的重排或重绘,上面介绍过,现代浏览器针对这种情况有性能的优化,它会合并DOM操作,但并不是所有的浏览器都存在这样的优化。推荐的方式是把DOM操作尽量合并,如上的代码可以优化为:
    1. // 优化方案1
    2. element.style.cssText += 'border: 1px solid #f00;';
    3. // 优化方案2
    4. element.className += 'empty';
    示例的代码有两种优化的方案,都做到了把多次的样式设置合并为一次设置。方案2比方案1稍微有一些性能上的损耗,因为它需要查询CSS类。但方案2的维护性最好,这在上一章曾经讨论过。很多时候,如果性能问题并不突出,选择编码方案时需要优先考虑的是代码的维护性。
    类似的操作还有通过innerHTML接口修改DOM元素的内容。不要直接通过此接口来拼接HTML代码,而是以字符串方式拼接好代码后,一次性赋值给DOM元素的innerHTML接口。

    2. 把DOM元素离线或隐藏后修改

    把DOM元素从页面流中脱离或隐藏,这样处理后,只会在DOM元素脱离和添加时,或者是隐藏和显示时才会造成页面的重绘或重排,对脱离了页面布局流的DOM元素操作就不会导致页面的性能问题。这种方式适合那些需要大批量修改DOM元素的情况。具体的方式主要有三种:
    (1)使用文档片段
    文档片段是一个轻量级的document对象,并不会和特定的页面关联。通过在文档片段上进行DOM操作,可以降低DOM操作对页面性能的影响,这 种方式是创建一个文档片段,并在此片段上进行必要的DOM操作,操作完成后将它附加在页面中。对页面性能的影响只存在于最后把文档片段附加到页面的这一步 操作上。代码类似如下:
    1. var fragment = document.createDocumentFragment();
    2. // 一些基于fragment的大量DOM操作
    3. ...
    4. document.getElementById('myElement').appendChild(fragment);
    (2)通过设置DOM元素的display样式为none来隐藏元素
    这种方式是通过隐藏页面的DOM元素,达到在页面中移除元素的效果,经过大量的DOM操作后恢复元素原来的display样式。对于这类会引起页面重绘或重排的操作,就只有隐藏和显示DOM元素这两个步骤了。代码类似如下:
    1. var myElement = document.getElementById('myElement');
    2. myElement.style.display = 'none';
    3. // 一些基于myElement的大量DOM操作
    4. ...
    5. myElement.style.display = 'block';
    (3)克隆DOM元素到内存中
    这种方式是把页面上的DOM元素克隆一份到内存中,然后再在内存中操作克隆的元素,操作完成后使用此克隆元素替换页面中原来的DOM元素。这样一来,影响性能的操作就只是最后替换元素的这一步操作了,在内存中操作克隆元素不会引起页面上的性能损耗。代码类似如下:
    1. var old = document.getElementById('myElement');
    2. var clone = old.cloneNode(true);
    3. // 一些基于clone的大量DOM操作
    4. ...
    5. old.parentNode.replaceChild(clone, old);
    在现代的浏览器中,因为有了DOM操作的优化,所以应用如上的方式后可能并不能明显感受到性能的改善。但是在仍然占有市场的一些旧浏览器中,应用以上这三种编码方式则可以大幅提高页面渲染性能。

    3. 设置具有动画效果的DOM元素的position属性为fixed或absolute

    把页面中具有动画效果的元素设置为绝对定位,使得元素脱离页面布局流,从而避免了页面频繁的重排,只涉及动画元素自身的重排了。这种做法可以提高动 画效果的展示性能。如果把动画元素设置为绝对定位并不符合设计的要求,则可以在动画开始时将其设置为绝对定位,等动画结束后恢复原始的定位设置。在很多的 网站中,页面的顶部会有大幅的广告展示,一般会动画展开和折叠显示。如果不做性能的优化,这个效果的性能损耗是很明显的。使用这里提到的优化方案,则可以 提高性能。

    4. 谨慎取得DOM元素的布局信息

    前面讨论过,获取DOM的布局信息会有性能的损耗,所以如果存在重复调用,最佳的做法是尽量把这些值缓存在局部变量中。考虑如下的一个示例:
    1. for (var i=0; i < len; i++) {
    2. myElements[i].style.top = targetElement.offsetTop + i*5 + 'px';
    3. }
    如上的代码中,会在一个循环中反复取得一个元素的offsetTop值,事实上,在此代码中该元素的offsetTop值并不会变更,所以会存在不必要的性能损耗。优化的方案是在循环外部取得元素的offsetTop值,相比较之前的方案,此方案只是调用了一遍元素的offsetTop值。更改后的代码如下:
  1. var targetTop = targetElement.offsetTop;
  2. for (var i=0; i < len; i++) {
  3. myElements[i].style.top = targetTop+ i*5 + 'px';
  4. }

另外,因为取得DOM元素的布局信息会强制浏览器刷新渲染树,并且可能会导致页面的重绘或重排,所以在有大批量DOM操作时,应避免获取DOM元素 的布局信息,使得浏览器针对大批量DOM操作的优化不被破坏。如果需要这些布局信息,最好是在DOM操作之前就取得。考虑如下一个示例:

  1. var newWidth = div1.offsetWidth + 10;
  2. div1.style.width = newWidth + 'px';
  3. var newHeight = myElement.offsetHeight + 10; // 强制页面重排
  4. myElement.style.height = newHeight + 'px'; // 又会重排一次

根据上面的介绍,代码在遇到取得DOM元素的信息时会触发页面重新计算渲染树,所以如上的代码会导致页面重排两次,如果把取得DOM元素的布局信息提前,因为浏览器会优化连续的DOM操作,所以实际上只会有一次的页面重排出现,优化后的代码如下:

  1. var newWidth = div1.offsetWidth + 10;
  2. var newHeight = myElement.offsetHeight + 10;
  3. div1.style.width = newWidth + 'px';
  4. myElement.style.height = newHeight + 'px';

5. 使用事件托管方式绑定事件

在DOM元素上绑定事件会影响页面的性能,一方面,绑定事件本身会占用处理时间,另一方面,浏览器保存事件绑定,所以绑定事件也会占用内存。页面中 元素绑定的事件越多,占用的处理时间和内存就越大,性能也就相对越差,所以在页面中绑定的事件越少越好。一个优雅的手段是使用事件托管方式,即利用事件冒 泡机制,只在父元素上绑定事件处理,用于处理所有子元素的事件,在事件处理函数中根据传入的参数判断事件源元素,针对不同的源元素做不同的处理。这样就不 需要给每个子元素都绑定事件了,管理的事件绑定数量变少了,自然性能也就提高了。这种方式也有很大的灵活性,可以很方便地添加或删除子元素,不需要考虑因 元素移除或改动而需要修改事件绑定。示例代码如下:

  1. // 获取父节点,并添加一个click事件
  2. document.getElementById('list').addEventListener("click",function(e) { // 检查事件源元素 if(e.target && e.target.nodeName.toUpperCase == "LI") { // 针对子元素的处理 ...
  3. }
  4. });

上述代码中,只在父元素上绑定了click事件,当点击子节点时,click事件会冒泡,父节点捕获事件后通过e.target检查事件源元素并做相应地处理。
在JavaScript中,事件绑定方式存在浏览器兼容问题,所以在很多框架中也提供了相似的接口方法用于事件托管。比如在jQuery中可以使用如下方式实现事件的托管(示例代码来自jQuery官方网站):

  1. $( "table" ).on( "click", "td", function() { $( this ).toggleClass( "chosen" );
  2. });