1. 这道题在考什么?
- 对于性能优化的处理方案
- 对于前端渲染机制的了解
- 极端情况下的处理及知识领域的广度
2.先用 node.js 整个10w条数据
const http = require('http') const PORT = 8000 const server = http.createServer((req, res) => { res.writeHead(200, { //设置允许跨域的域名,也可设置*允许所有域名 'Access-Control-Allow-Origin': '*', //跨域允许的请求方法,也可设置*允许所有方法 "Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS", //允许的header类型 'Access-Control-Allow-Headers': 'Content-Type' }) let list = []; let num = 0; for (let i = 0; i < 200; i++) { num++; list.push({ src: '图片地址', text: `这是第${num}张图片`, id: num }) } res.end(JSON.stringify(list)) }) server.listen(PORT, () => { console.log('服务跑起来了!') })
3. 基础代码环境
- index.html 代码如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> img{ width: 150px; } .flex{ display: flex; padding: 10px; } </style> </head> <body> <div id="container"> </div> <script src="./index.js"></script> </body> </html>
- index.js 代码如下
const getList = () => { return new Promise((resolve, reject) => { // 创建请求 let ajax = new XMLHttpRequest(); // 这里请求的是本地服务器 ajax.open('get', 'http://127.0.0.1:8000'); ajax.send(); ajax.onreadystatechange = function(){ if(ajax.readyState == 4 && ajax.status == 200){ resolve(JSON.parse(ajax.responseText)) } } }) } const container = document.getElementById("container")
4. 常规处理方案
const renderList = async () => { console.time('列表时间') const list = await getList(); list.forEach( item => { const div = document.createElement('div') div.className = 'flex' div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>` container.appendChild(div) }); console.timeEnd('列表时间') } renderList()
- 这种方案就是简单粗暴的循环渲染
- 此方案耗时大概是 13s
- 这种做法当然是不可取的,等到天都黑了,用户可能会骂娘
5. 优化的第一种方式 —— 前端分页
const renderList = async () => { console.time('列表时间') const list = await getList(); const total = list.length; const page = 0; const limit = 200; // 总页数 const totalPage = Math.ceil(total / limit); // Math.ceil 1.1 => 2 const render = (page) => { if(page >= totalPage) return setTimeout(() => { for(let i = page * limit; i < page * limit + limit; i++){ const item = list[i]; const div = document.createElement('div') div.className = 'flex' div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>` container.appendChild(div) } render(page + 1) }, 0) } render(page); console.timeEnd('列表时间') } renderList()
- 思路是把十万条数据分成 10w / 200页,再加上setTimeout,每次渲染一页,速度得到了大幅度提升。
- 方案耗时:不到 1s 搞定
6. 再次优化
const renderList = async () => { console.time('列表时间') const list = await getList(); const total = list.length; const page = 0; const limit = 200; // 总页数 const totalPage = Math.ceil(total / limit); // Math.ceil 1.1 => 2 const render = (page) => { if(page >= totalPage) return requestAnimationFrame(() => { for(let i = page * limit; i < page * limit + limit; i++){ const item = list[i]; const div = document.createElement('div') div.className = 'flex' div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>` container.appendChild(div) } render(page + 1) }) } render(page); console.timeEnd('列表时间') } renderList()
- 使用 requestAnimationFrame 代替 setTimeout,减少了重排的次数,极大提高了性能
7. 极致优化(最佳方案)
const renderList = async () => { console.time('列表时间') const list = await getList(); const total = list.length; const page = 0; const limit = 200; // 总页数 const totalPage = Math.ceil(total / limit); // Math.ceil 1.1 => 2 const render = (page) => { if(page >= totalPage) return requestAnimationFrame(() => { const fragment = document.createDocumentFragment() // 文档碎片 => dom节点 不是在dom树上一部分 // N次追加 => 1次 for(let i = page * limit; i < page * limit + limit; i++){ const item = list[i]; const div = document.createElement('div') div.className = 'flex' div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>` fragment.appendChild(div) // container.appendChild(div) } container.appendChild(fragment) render(page + 1) }) } render(page); console.timeEnd('列表时间') } renderList()
- 这里的优化点主要是:之前是创建一个div就追加一次:
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>` container.appendChild(div)
- 现在改成一次性追加,极大的提高了性能。
container.appendChild(fragment)
8. 知识点补充
window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行;
若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()
DocumentFragments —— 文档碎片
DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。
在DOM树中,文档片段被其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中。
所以将子元素插入到文档片段时不会引起【页面回流】(对元素位置和几何上的计算)。
因此,使用文档片段通常会带来更好的性能。