面試官: 後端一次返回過多資料,前端應該如何優化處理?

語言: CN / TW / HK

大廠技術    高階前端    Node進階

點選上方  程式設計師成長指北 ,關注公眾號

回覆 1 ,加入高階Node交流

英文 | 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

最近,我的一位朋友在面試時被問到這個問題。這個問題其實是考察面試者對效能優化的理解,涉及的話題很多。下面我就和大家一起來分析一下這個問題。

建立伺服器

為了方便後續測試,我們可以使用node建立一個簡單的伺服器。

伺服器端程式碼:

const http = require('http')

const port = 8000;



let list = []

let num = 0



// create 100,000 records

for (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 啟動伺服器:

$ node server.js

# or

$ nodemon server.js

建立前端模板頁面

然後我們的前端由一個 HTML 檔案和一個 JS 檔案組成。

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>

* {

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 server

const 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` element

const container = document.getElementById('container')





// The rendering logic should be written here.

好的,這就是我們的前端頁面模板程式碼,我們開始渲染資料。

直接渲染

最直接的方法是一次將所有資料渲染到頁面。程式碼如下:

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 進行分頁渲染

一個簡單的優化方法是對資料進行分頁。假設每個頁面都有limit記錄,那麼資料可以分為Math.ceil(total/limit)個頁面。之後,我們可以使用 setTimeout 順序渲染頁面,一次只渲染一個頁面。

const renderList = async () => {



const list = await getList()



const total = list.length

const page = 0

const limit = 200

const totalPage = Math.ceil(total / limit)



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 = 'sunshine'

div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`

container.appendChild(div)

}

render(page + 1)

}, 0)

}

render(page)

}

分頁後,資料可以快速渲染到螢幕上,減少頁面的空白時間。

requestAnimationFrame

在渲染頁面的時候,我們可以使用requestAnimationFrame來代替setTimeout,這樣可以減少reflow次數,提高效能。

const renderList = async () => {

const list = await getList()



const total = list.length

const page = 0

const limit = 200

const totalPage = Math.ceil(total / limit)



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 = 'sunshine'

div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`

container.appendChild(div)

}

render(page + 1)

})

}

render(page)

}

window.requestAnimationFrame() 方法告訴瀏覽器您希望執行動畫,並請求瀏覽器呼叫指定函式在下一次重繪之前更新動畫。該方法將回調作為要在重繪之前呼叫的引數。

文件片段

以前,每次建立 div 元素時,都會通過 appendChild 將元素直接插入到頁面中。但是 appendChild 是一項昂貴的操作。

實際上,我們可以先建立一個文件片段,在建立了 div 元素之後,再將元素插入到文件片段中。建立完所有 div 元素後,將片段插入頁面。這樣做還可以提高頁面效能。

const renderList = async () => {

console.time('time')

const list = await getList()

console.log(list)

const total = list.length

const page = 0

const limit = 200

const totalPage = Math.ceil(total / limit)



const render = (page) => {

if (page >= totalPage) return

requestAnimationFrame(() => {



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')

}

延遲載入

雖然後端一次返回這麼多資料,但使用者的螢幕只能同時顯示有限的資料。所以我們可以採用延遲載入的策略,根據使用者的滾動位置動態渲染資料。

要獲取使用者的滾動位置,我們可以在列表末尾新增一個空節點空白。每當視口出現空白時,就意味著使用者已經滾動到網頁底部,這意味著我們需要繼續渲染資料。

同時,我們可以使用getBoundingClientRect來判斷空白是否在頁面底部。

使用 Vue 的示例程式碼:

<script setup lang="ts">

import { onMounted, ref, computed } from 'vue'

const getList = () => {

// code as before

}

const container = ref<HTMLElement>() // container element

const blank = ref<HTMLElement>() // blank element

const list = ref<any>([])

const page = ref(1)

const limit = 200

const maxPage = computed(() => Math.ceil(list.value.length / limit))

// List of real presentations

const showList = computed(() => list.value.slice(0, page.value * limit))

const handleScroll = () => {

if (page.value > maxPage.value) return

const clientHeight = container.value?.clientHeight

const blankTop = blank.value?.getBoundingClientRect().top

if (clientHeight === blankTop) {

// When the blank node appears in the viewport, the current page number is incremented by 1

page.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>

最後

我們從一個面試問題開始,討論了幾種不同的效能優化技術。

如果你在面試中被問到這個問題,你可以用今天的內容回答這個問題,如果你在工作中遇到這個問題,你應該先揍那個寫 API 的人。

Node 社群

我組建了一個氛圍特別好的 Node.js 社群,裡面有很多 Node.js小夥伴,如果你對Node.js學習感興趣的話(後續有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回覆「Node」即可。

如果你覺得這篇內容對你有幫助,我想請你幫我2個小忙:

1. 點個 「在看」 ,讓更多人也能看到這篇文章

2. 訂閱官方部落格  www.inode.club  讓我們一起成長

點贊和在看就是最大的支援