如果后端API一次返回10萬條數(shù)據(jù),前端應(yīng)該如何處理?
點擊上方 前端Q,關(guān)注公眾號
回復(fù)加群,加入前端Q技術(shù)交流群
英文 | https://medium.com/frontend-canteen/if-the-backend-api-returns-100-000-records-at-one-time-how-should-we-handle-it-in-the-frontend-fab21218fe2
最近,我的一位朋友在面試時被問到這個問題。這個問題其實是考察面試者對性能優(yōu)化的理解,涉及的話題很多。下面我就和大家一起來分析一下這個問題。
創(chuàng)建服務(wù)器
為了方便后續(xù)測試,我們可以使用node創(chuàng)建一個簡單的服務(wù)器。
服務(wù)器端代碼:
const http = require('http')const port = 8000;let list = []let num = 0// create 100,000 recordsfor (let i = 0; i < 100_000; i++) {num++list.push({src: 'https://miro.medium.com/fit/c/64/64/1*XYGoKrb1w5zdWZLOIEevZg.png',text: `hello world ${num}`,tid: num})}http.createServer(function (req, res) {// for Cross-Origin Resource Sharing (CORS)res.writeHead(200, {'Access-Control-Allow-Origin': '*',"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",'Access-Control-Allow-Headers': 'Content-Type'})res.end(JSON.stringify(list));}).listen(port, function () {console.log('server is listening on port ' + port);})
我們可以使用 node 或 nodemon 啟動服務(wù)器:
node server.jsornodemon server.js
創(chuàng)建前端模板頁面
然后我們的前端由一個 HTML 文件和一個 JS 文件組成。
Index.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>* {padding: 0;margin: 0;}#container {height: 100vh;overflow: auto;}.sunshine {display: flex;padding: 10px;}img {width: 150px;height: 150px;}</style></head><body><div id="container"></div><script src="./index.js"></script></body></html>
Index.js:
// fetch data from the serverconst getList = () => {return new Promise((resolve, reject) => {var 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))}}})}// get `container` elementconst container = document.getElementById('container')// The rendering logic should be written here.
好的,這就是我們的前端頁面模板代碼,我們開始渲染數(shù)據(jù)。
直接渲染
最直接的方法是一次將所有數(shù)據(jù)渲染到頁面。代碼如下:
const renderList = async () => {const list = await getList()list.forEach(item => {const div = document.createElement('div')div.className = 'sunshine'div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`container.appendChild(div)})}renderList()
一次渲染 100,000 條記錄大約需要 12 秒,這顯然是不可取的。
通過 setTimeout 進(jìn)行分頁渲染
一個簡單的優(yōu)化方法是對數(shù)據(jù)進(jìn)行分頁。假設(shè)每個頁面都有l(wèi)imit記錄,那么數(shù)據(jù)可以分為Math.ceil(total/limit)個頁面。之后,我們可以使用 setTimeout 順序渲染頁面,一次只渲染一個頁面。
const renderList = async () => {const list = await getList()const total = list.lengthconst page = 0const limit = 200const totalPage = Math.ceil(total / limit)const render = (page) => {if (page >= totalPage) returnsetTimeout(() => {for (let i = page * limit; i < page * limit + limit; i++) {const item = list[i]const div = document.createElement('div')div.className = 'sunshine'div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`container.appendChild(div)}render(page + 1)}, 0)}render(page)}
分頁后,數(shù)據(jù)可以快速渲染到屏幕上,減少頁面的空白時間。
requestAnimationFrame
在渲染頁面的時候,我們可以使用requestAnimationFrame來代替setTimeout,這樣可以減少reflow次數(shù),提高性能。
const renderList = async () => {const list = await getList()const total = list.lengthconst page = 0const limit = 200const totalPage = Math.ceil(total / limit)const render = (page) => {if (page >= totalPage) returnrequestAnimationFrame(() => {for (let i = page * limit; i < page * limit + limit; i++) {const item = list[i]const div = document.createElement('div')div.className = 'sunshine'div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`container.appendChild(div)}render(page + 1)})}render(page)}
window.requestAnimationFrame() 方法告訴瀏覽器您希望執(zhí)行動畫,并請求瀏覽器調(diào)用指定函數(shù)在下一次重繪之前更新動畫。該方法將回調(diào)作為要在重繪之前調(diào)用的參數(shù)。
文檔片段
以前,每次創(chuàng)建 div 元素時,都會通過 appendChild 將元素直接插入到頁面中。但是 appendChild 是一項昂貴的操作。
實際上,我們可以先創(chuàng)建一個文檔片段,在創(chuàng)建了 div 元素之后,再將元素插入到文檔片段中。創(chuàng)建完所有 div 元素后,將片段插入頁面。這樣做還可以提高頁面性能。
const renderList = async () => {console.time('time')const list = await getList()console.log(list)const total = list.lengthconst page = 0const limit = 200const totalPage = Math.ceil(total / limit)const render = (page) => {if (page >= totalPage) returnrequestAnimationFrame(() => {const fragment = document.createDocumentFragment()for (let i = page * limit; i < page * limit + limit; i++) {const item = list[i]const div = document.createElement('div')div.className = 'sunshine'div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`fragment.appendChild(div)}container.appendChild(fragment)render(page + 1)})}render(page)console.timeEnd('time')}
延遲加載
雖然后端一次返回這么多數(shù)據(jù),但用戶的屏幕只能同時顯示有限的數(shù)據(jù)。所以我們可以采用延遲加載的策略,根據(jù)用戶的滾動位置動態(tài)渲染數(shù)據(jù)。
要獲取用戶的滾動位置,我們可以在列表末尾添加一個空節(jié)點空白。每當(dāng)視口出現(xiàn)空白時,就意味著用戶已經(jīng)滾動到網(wǎng)頁底部,這意味著我們需要繼續(xù)渲染數(shù)據(jù)。
同時,我們可以使用getBoundingClientRect來判斷空白是否在頁面底部。

使用 Vue 的示例代碼:
<script setup lang="ts">import { onMounted, ref, computed } from 'vue'const getList = () => {// code as before}const container = ref<HTMLElement>() // container elementconst blank = ref<HTMLElement>() // blank elementconst list = ref<any>([])const page = ref(1)const limit = 200const maxPage = computed(() => Math.ceil(list.value.length / limit))// List of real presentationsconst showList = computed(() => list.value.slice(0, page.value * limit))const handleScroll = () => {if (page.value > maxPage.value) returnconst clientHeight = container.value?.clientHeightconst blankTop = blank.value?.getBoundingClientRect().topif (clientHeight === blankTop) {// When the blank node appears in the viewport, the current page number is incremented by 1page.value++}}onMounted(async () => {const res = await getList()list.value = res})</script><template><div id="container" @scroll="handleScroll" ref="container"><div class="sunshine" v-for="(item) in showList" :key="item.tid"><img :src="item.src" /><span>{{ item.text }}</span></div><div ref="blank"></div></div></template>
最后
我們從一個面試問題開始,討論了幾種不同的性能優(yōu)化技術(shù)。
如果你在面試中被問到這個問題,你可以用今天的內(nèi)容回答這個問題,如果你在工作中遇到這個問題,你應(yīng)該先揍那個寫 API 的人。

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...


