6

Create server

To facilitate subsequent testing, we can use node to create a simple server.

Server side code:

 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);
})

We can start the server using node or nodemon:

 $ node server.js
# or 
$ nodemon server.js

Create a front-end template page

Then our front end consists of an HTML file and a JS file.

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.

Ok, this is our front-end page template code, we start rendering the data.

direct rendering

The most straightforward way is to render all the data to the page at once. code show as below:

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

It takes about 12 seconds to render 100,000 records at a time, which is obviously not desirable.

1. Paging rendering through setTimeout

A simple optimization is to paginate the data. Assuming that each page has limit records, the data can be divided into Math.ceil(total/limit) pages. After that, we can use setTimeout to render pages sequentially, one page at a time.

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

After paging, the data can be quickly rendered to the screen, reducing the blank time of the page.

2. requestAnimationFrame

When rendering the page, we can use requestAnimationFrame instead of setTimeout, which can reduce the number of reflows and improve performance.

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

The window.requestAnimationFrame() method tells the browser that you want to perform the animation, and requests the browser to call the specified function to update the animation before the next repaint. The method takes a callback as a parameter to be called before repainting.

3. Document Fragments

Previously, every time a div element was created, the element was inserted directly into the page via appendChild. But appendChild is an expensive operation.

In fact, we could create a document fragment first, and after creating the div element, insert the element into the document fragment. After all the div elements are created, insert the fragment into the page. Doing so can also improve page performance.

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

4. Lazy loading

While the backend returns so much data at once, the user's screen can only display a limited amount of data at the same time. So we can use a lazy loading strategy to dynamically render data based on the user's scrolling position.

To get the user's scroll position, we can add an empty node blank at the end of the list. Whenever the viewport is blank, it means the user has scrolled to the bottom of the page, which means we need to continue rendering data.

At the same time, we can use getBoundingClientRect to determine whether the blank is at the bottom of the page.

Example code using 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>

墨城
1.7k 声望2.1k 粉丝

« 上一篇
Vue基础点梳理
下一篇 »
树形数据转换