Frontend Development 13 min read

Efficiently Rendering 100,000 Items with Time Slicing and Virtual List in Frontend Development

This article explains how to avoid UI freezes when rendering massive data sets in the browser by using time‑slicing techniques such as setTimeout or requestAnimationFrame together with document fragments, and by implementing a virtual list component in Vue to render only the visible portion of the data.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Efficiently Rendering 100,000 Items with Time Slicing and Virtual List in Frontend Development

Introduction

In frontend development, rendering a huge amount of data (e.g., 100,000 rows) at once can freeze the UI, cause lag, or even crash the browser. This article introduces two complementary solutions—time slicing and virtual lists—to render large datasets smoothly and pass interview questions about performance optimization.

Prerequisite Knowledge

JavaScript runs on a single thread and follows the event‑loop model, which processes tasks in the order of macro‑tasks, micro‑tasks, and then rendering. Understanding the distinction between micro‑tasks (Promise.then, async/await, etc.) and macro‑tasks (script execution, setTimeout, I/O, etc.) is essential for the techniques described later.

Time Slicing

The core idea of time slicing is to split a large rendering job into many small chunks so that the browser’s rendering thread can interleave data processing with painting, keeping the UI responsive.

Using setTimeout

Initialization : define total data count ( total ), items per batch ( once ), total batches ( page ) and current index ( index ).

Recursive Rendering : a loop function renders once items per call and schedules the next batch with setTimeout .

Timer : setTimeout moves each batch to the next event loop, preventing the main thread from being blocked.

Termination : stop recursion when curTotal - pageCount <= 0 .

let ul = document.getElementById('container');
const total = 100000; // total items
let once = 20; // items per batch
let page = total / once;
let index = 0;
function loop(curTotal, curIndex) {
  let pageCount = Math.min(once, curTotal);
  setTimeout(() => {
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement('li');
      li.innerText = curIndex + i + ':' + ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount, curIndex + pageCount);
  });
}
loop(total, index);

By spreading the work over many event‑loop cycles, the browser renders only a small number of items per frame, dramatically reducing layout thrashing and improving perceived performance.

Using requestAnimationFrame

When the rendering interval (≈16.7 ms) does not align with the JavaScript execution time, setTimeout may cause desynchronization, leading to flicker. Replacing it with requestAnimationFrame synchronizes rendering with the browser’s paint cycle.

To further reduce reflows, a document fragment ( document.createDocumentFragment() ) batches DOM insertions, ensuring that only one reflow occurs per batch of once items.

let ul = document.getElementById('container');
const total = 100000;
let once = 20;
let page = total / once;
let index = 0;
function loop(curTotal, curIndex) {
  let pageCount = Math.min(once, curTotal);
  requestAnimationFrame(() => {
    let fragment = document.createDocumentFragment();
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement('li');
      li.innerText = curIndex + i + ':' + ~~(Math.random() * total);
      fragment.appendChild(li);
    }
    ul.appendChild(fragment);
    loop(curTotal - pageCount, curIndex + pageCount);
  });
}
loop(total, index);

Virtual List

A virtual list renders only the items that are currently visible in the viewport, drastically reducing the number of DOM nodes and improving scroll performance.

Core Steps

Initialize container and data source.

Calculate visible area based on container height and item height.

Render the initial visible slice.

Listen to scroll events to update start/end indices.

Adjust container offset to keep the list aligned.

Vue Implementation

App.vue (root component) defines a data array of 1,000 objects and passes it to a custom virtualList component.

<template>
  <div class="app">
    <virtualList :listData="data" />
  </div>
</template>

<script setup>
import { ref } from 'vue';
import virtualList from './components/virtualList.vue';
const data = ref([]);
for (let i = 0; i < 1000; i++) {
  data.value.push({ id: i, value: i });
}
</script>

<style scoped>
.app{ width:300px; height:400px; border:1px solid #000; }
</style>

virtualList.vue (custom component) handles the scrolling logic, computes visible data, and uses a document fragment‑like approach via CSS transforms.

<template>
  <div ref="listRef" class="infinite-list-container" @scroll="scrollEvent()">
    <div class="infinite-list-phantom" :style="{height: listHeight + 'px'}"></div>
    <div class="infinite-list" :style="{transform: getTransform}">
      <div class="infinite-list-item" v-for="item in visibleData" :key="item.id"
           :style="{height: itemSize + 'px', lineHeight: itemSize + 'px'}">
        {{ item.value }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted, reactive, ref, computed } from 'vue';
const props = defineProps({
  listData: Array,
  itemSize: { type: Number, default: 50 }
});
const state = reactive({ screenHeight:0, startOffset:0, start:0, end:0 });
const visibleCount = computed(() => state.screenHeight / props.itemSize);
const visibleData = computed(() => props.listData.slice(state.start, Math.min(props.listData.length, state.end)));
const listHeight = computed(() => props.listData.length * props.itemSize);
const getTransform = computed(() => `translateY(${state.startOffset}px)`);
const listRef = ref(null);
onMounted(() => {
  state.screenHeight = listRef.value.clientHeight;
  state.end = state.start + visibleCount.value;
});
const scrollEvent = () => {
  const scrollTop = listRef.value.scrollTop;
  state.start = Math.floor(scrollTop / props.itemSize);
  state.end = state.start + visibleCount.value;
  state.startOffset = scrollTop - (scrollTop % props.itemSize);
};
</script>

<style scoped>
.infinite-list-container{ height:100%; overflow:auto; position:relative; }
.infinite-list-phantom{ position:absolute; left:0; top:0; right:0; z-index:-1; }
.infinite-list{ position:absolute; left:0; top:0; right:0; text-align:center; }
.infinite-list-item{ border-bottom:1px solid #000; box-sizing:border-box; }
</style>

Conclusion

Time Slicing : By separating data generation (JS thread) and DOM painting (render thread) with setTimeout or requestAnimationFrame , and by using document fragments, large lists can be rendered without blocking the UI.

Virtual List : Calculating container height, visible item count, and rendering only the viewport slice dramatically reduces DOM size and improves scroll smoothness.

Other techniques such as lazy loading and Web Workers can further optimize massive data rendering.

frontendPerformanceJavaScriptVuevirtual-listTime Slicing
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.