超长列表性能优化

超长列表性能优化

今天大概内容会讲一下,js中基础知识,会讲一下怎么是实现代码的切片,怎么渲染切片,完了之后会说一下最核心的虚拟列表,用Vue写的虚拟列表。包括定高的和不定高的怎么去实现。

【引言】长列表优化有两种策略,第一种就是我们可以分片,比如一个很长的列表,我可以分片渲染,第二就是我只渲染可视区域

必备理论知识】第一先聊一下JS中基本的概念,不会深说,但是会把用得到的深说一下。第一先我们要掌握一下什么是进程,这个概念比较大,当前一个系统,系统每开一个应用,每一个应用程序都会分配一个独立的进程,当然一个应用有很多进程。但是我们先不说进程,主要把目光说到线程上,其实一个进程内包含多个线程,比如说我们写js代码时候说js是单线程的,其实这个说的不是很明确,那叫JS的主线程是单线程的,我们写代码的时候最关心的是浏览器内核,也就是我们的渲染进程,一个进程包含多个线程,一个渲染进程就包含多个线程,比如说GUI渲染线程,像CSS我可以把CSS渲染到页面上,用的就是我们的GUI渲染线程,再就是我们的JS引擎线程,它主要帮我们执行JS脚本的,GUI渲染线程和js引擎线程是互斥的,在我们运行JS的过程中,它是无法进行页面渲染的,同样在页面渲染的时候js也是无法执行的所以他们两个之间是互斥关系,当一个开起的时候另一个会被挂起,这是两个特点。我们指的单线程就是GUI渲染线程和JS引擎线程共用的线程,在执行过程中会跳到JS引擎线程执行,在JS执行的过程其实我们可以写很多创建线程的代码,比如说我们可以写一个事件(onclick),它会单独的创建一个线程,比如说我会在代码里写一个定时器,它也会单独的开一个线程,包括ajax它也会单独开一个线程,这些东西它都很新开一个线程,其实在我们的渲染进程里包含着很多线程,我们所说的JS是单线程的,指的是主线程是单线程的,在代码执行过程中,我们还是可以创建很多其他线程,等一会等我们主线程里的代码都执行完了,还有一个叫事件触发线程,这个是前端的核心概念,包括后端也有,叫EventLoop,相当于当我们同步代码执行完之后,会去取异步代码让它去执行,先明确概念,我们通过一张图来看。这里面一再强调JS是单线程的?为什么是单线程,因为我们都知道js页面里面有很多DOM元素,如果js是多线程的话,可能会导致多个线程操作同一个DOM,那这样之后就会有很多问题了。比如说一个线程说我希望增加一个DOM,另一个线程说我希望删除这个DOM,到底是增加还是减少,不好说,js项目里也没有什么锁的概念,所以说JS是单线程的,webworker确实是开了一个线程,但是它不能操作DOM,你可以叫它辅线程、子线程。

一.什么是进程?什么是线程?

  • 进程是系统进行资源分配和调度的一个独立单位, 一个进程内包含多个线程

二.渲染进程

  • GUI渲染线程(页面渲染)
  • JS引擎线程(执行js脚本的)
  • 事件触发线程(EventLoop轮询处理线程)
  • 事件( onclick),定时器( setTimeout), ajax(xhr) (独立线程)
    GUI渲染线程和JS引擎线程互斥的,
    我们所谓的JS为什么是单线程的? webworker
    一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

三.浏览器中的EventLoop

在这里插入图片描述
刚开始我们执行代码的时候有一个JS引擎线程,其实你可以认为就是我们写代码时候默认的执行栈,我们写代码的时候可以放很多函数、很多变量、包括调ajax、或者调定时器,我们代码里包含着很多同步代码和异步代码,我们把这个空间默认执行的时候叫它是一个宏任务,所谓叫本执行是一个宏任务。在代码执行的过程中会有很多api,大家都知道这些api是异步的,比如说AJAX、定时器、click事件,他们都是异步的。包括一些其他的像Promise.then、还有MutationObserver。把这两个东西进行分类,像ajax,定时器、还有event。Promise.thenMutationObserver分成一类,他们有什么区别?当用户如果调了Promise.thenMutationObserver的时候,我们会将它里面的回调放到微任务队列。先明确,只要调用这些方法,他就会把Promise.then的回调,MutationObserver的回调放到一个微任务队列里去,同时如果用户调了一些比如说ajax、定时器、包括event按钮,它并不会立即将这些回调放进去。那什么时候放进去?比如说ajax成功了,比如说定时器到时间了,等一会我点的时候,再把这个函数放到队列里去,这个队列叫宏任务队列。我们主线程整个执行完之后,异步代码,同步代码都执行完了,这时候它会做一件事情,他会清空微任务队列,而且明确,只要用户一调,Promise.thenMutationObserver会存到微任务队列、代码执行完成之后他会清空这些队列、清完之后,这里面会做一件事情,JS引擎执行完了,是不是开始渲染页面了,这时候会调用我们的GUI线程去渲染页面。渲染完成之后,它会再我们的宏任务队列里看一下,看有没有一个事件到时间了,主要靠的就是我们的EventLoop事件,它主要做的就是事件触发线程,它会去查,看宏任务有没有到时间的,有没有完成的,有没有东西。如果有东西,它会取出一个宏任务,它会把它放到JS栈中接着执行,而且这些过程,当我们执行定时器的回调里或者ajax的回调里里面可能还会再去添加异步方法、一些异步事件、包括还有promise,这时候依然会按我们这个流程继续次执行下去,这是一个无线循环的过程。靠的就是一个事件触发线程来做到的。这里主要强调一件事情,就是GUI渲染线程的时机它是怎么渲染的?是在我们JS执行完之后包括微任务清空之后、在下一个宏任务执行之前,它会做一次渲染。默认脚本执行是一个宏任务,当我们开个定时器也是一个宏任务,在定时器执行之前就会渲染一次页面。微任务产生的宏任务也放到宏任务队列中,微任务产生的微任务会立即执行,知道把前的微任务队列清空完毕。JS引擎线程执行完之后,被立即执行的是微任务,当微任务清空之后会去渲染页面,渲染页面后会去调定时器和ajax做这些事情requestanimationFrame也是宏任务。具体执行的时候是先执行宏任务,再去执行微任务,因为JS引擎线程它就是一个宏任务,宏任务执行完之后它会清空宏微任务任务,清空完后,它会再去走GUI渲染线程,相当于又是一个宏任务,相当于,宏任务(js引擎线程),微任务,宏任务(GUI)线程,这个执行完之后,相当于在我们宏任务队列里再拿出一个来执行。而且每次只会清空一个去执行,如果有很多宏任务,它是一个个去执行。宏任务里有宏任务,它会去调方法,把这方法时间到了放宏任务队列里去。
**【宏任务就不会走GUI线程的吗?】**从宏任务里取第一个执行,操作完DOM,它会清空微任务,再去渲染,直到宏任务里没有东西了,微任务里面没东西了,JS就走完了,就结束了。是个环。

[js不是单线程的,它的主线程是单线程的],在执行的过程会调一些api,这些api会再去开一个线程。

我们代码执行过程中,里面可能会有同步代码和异步代码,异步代码分成两类,一类是微任务,例如Promise.then、MutationObserver,一调微任务,会立即将它的回调函数放到微任务队列里。如果你的异步代码,像ajax和定时器,它不会立即放到微任务队列里,而是等大当前时间到了,或者请求回来了,再放到队列里。JS代码执行完成之后会立即去清空微任务队列,微任务队列会马上执行,如果微任务里面再有微任务呢,它也一样会往后排,会整个把微任务队列清空完成之后再去渲染页面。页面渲染完成之后,我们事件触发线程会去扫描宏任务队列,如果这个队列里还有内容的话,会取出来接着去执行,这里面可能还和刚才流程是一模一样的,可能有宏任务,宏任务往里放,微任务往里放,代码执行完成之后。再去清空微任务队列,再去渲染页面,再去取下一个宏任务,微任务是批量执行,宏任务每次只执行一个。

【问题小节】
event是单独起一个线程。
【滚动加载方式可取吗】
滚动加载最后都把dom留到页面上了,我们希望页面的dom尽可能少

四.超长列表性能优化

  • 分片渲染(通过浏览器事件环机制,分别渲染时间)
  • 虚拟列表(只渲染可视区域)
<body>
<div>
    <div id="container"></div>
    <script>
      let total = 100000;
      for(let i= 0;i<total;i++){
        let li = document.createElement('li');
        li.innerHTML = i;
        container.appendChild(li)
      }
      console.log(Date.now()-timer);//这样算不是渲染时间,而是我们当前for循环执行的时间
    </script>
</body>

在这里插入图片描述
打开浏览器,页面顶部在转动,你会发现,时间1S已经打印出来,但是页面还是白的。这是因为它不是一个个把内容扔到页面上,它是把这些东西都存起来,最后一起仍到页面上。这是新版本浏览器的优化,如果我们每次往页面上扔一个西的话,页面就会重绘,但我们浏览器会循环操作,这时候它并不会添一条,加一条,而是把所有的js都执行完之后,新版本浏览器的优化,当js执行完后会一并插入到页面中。js先执行完,渲染要花很久。想统计一下,这十万条渲染要多久?怎么知道它渲染多久?渲染过程是在js执行完之后,下一个宏任务执行之前。
[原理:渲染的过程是在下一个任务执行之前]所以可以这样统计渲染时间,再设置一个定时器,script是一个宏任务,setTimeout也是一个宏任务,setTimeout之前就是渲染过程花的时间,如图两者之差便是我们渲染的时间,如果把十万条数据扔到页面上,渲染花费的时间还是很长的。在控制台可以看到是7S,为了解决这个渲染时间过长的问题。

 <div>
    <div id="container"></div>
    <script>
      let total = 100000;
      //新版本浏览器的优化,当js执行完会一并插入到页面中
      let timer = Date.now()
      for(let i= 0;i<total;i++){
        let li = document.createElement('li');
        li.innerHTML = i;
        container.appendChild(li)
      }
      console.log(Date.now()-timer);//这样算不是渲染时间,而是我们for循环执行的时间
      setTimeout(()=>{
         console.log(Date.now()-timer);
      },0)
    </script>

在这里插入图片描述

分片渲染

我们肯定希望用户尽快的看到结果,所以就有一个方案,所以就有了一种方案分片,我们可以根据数据大小划分每次加载固定的数量大小,我就希望十万条,每次渲染50条,50条渲染完之后,再渲染50条,这时候就用到了刚才的事件环机制了,当我们定时器完成之后,再开一个定时器,再渲染就可以实现递归操作了。如以下这样,这个过程没有什么意义,其实和上面结果一样是没有区别的,没有进行优化,因为先加载50条,它会等吗?不会。它会攒着,50…50…,一口气渲染到页面上,所以根本不会有优化过程。

 <body>

    <div id="container"></div>
    <script>
      let total = 100000;
      //新版本浏览器的优化,当js执行完会一并插入到页面中
      let timer = Date.now()
      for(let i= 0;i<total;i++){
        let li = document.createElement('li');
        li.innerHTML = i;
        container.appendChild(li)
      }
      console.log(Date.now()-timer);//这样算不是渲染时间,而是我们for循环执行的时间
      setTimeout(()=>{
         console.log(Date.now()-timer);
      },0);
      // 分片,我们可以根据数据大小划分 每次加载固定的数量
    </script>
 
</body>

我们希望它完成分片,希望等待50个完成之后,再去开50个。加一个定时器就不一样了,我们加载完成之后.js代码执行好了,它会开一个定时器,这个定时器会渲染50个列表,这时候50个渲染完成之后,开始渲染了,因为它再去调load()方法的时候,又会开一个定时器,当前的宏任务会等待上一次宏任务完成之后,才会渲染第二批,那我就做到分片加载。打开浏览器,瞬间就出来了。它是先加载50个再加载50个,往下拖就会有白屏的效果。这时候还是有问题,这时候DOM过多,就会造成浏览器卡顿。

<body>
  <div id="container"></div>
  <script>

    let total = 100000;
    //新版本浏览器的优化,当js执行完会一并插入到页面中
    let index = 0;//偏移量
    let id = 0;//递增的内容
    function load() {
      index += 50;
      if (index < total) { 
        //setTimeout是一个宏任务 requestAnimationFrame也是一个宏任务 ,可以配合浏览器的刷新频率
        //而且性能比setTimeout性能更好一些,做动画为了动画流畅会用它,所以可以直接换掉,效果不是很明显
        requestAnimationFrame(() => {//分片渲染  因为定时器是一个宏任务 会等待ui渲染完成后执行,
  //先渲染50个等待渲染完毕后,再渲染50个,就实现了一个分片加载
  //插入页面的时候,尽量不要使用li直接往里面参与,ie浏览器的低版本,你往里一塞,就会重新绘制界面;新版本是等执行完后,一口气仍到页面里去
  //为了ie低版本浏览器的优化,需要使用文档碎片
        let fragment = document.createDocumentFragment();
          for (let i = 0; i < 50; i++) {
            let li = document.createElement('li');
            li.innerHTML = id++;
            fragment.appendChild(li);
          }
          container.appendChild(fragment)
          load()
        }, 0);
        load()
      }
    }
    load();
  //分片加载  会导致页面都dom元素过多造成页面的卡顿
 //虚拟列表优化,只渲染当前的可视区域
  </script>

</body>
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值