Please teach me how to make a virtual scroll. I use HTML, JS, Vue. I tried using vue-virtual-scroll, but since it is difficult to change it to the function I want, I'm going to make a basic part and apply it. Please tell me how to make a basic virtual scroll.
Asked
Active
Viewed 1.2k times
6
-
1You can start from here: https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib – Alex Brohshtut Mar 30 '20 at 06:29
-
Check out this: https://github.com/Akryum/vue-virtual-scroller – beingyogi Mar 30 '20 at 08:53
-
[Virtual scrolling: Core principles and basic implementation in React](https://blog.logrocket.com/virtual-scrolling-core-principles-and-basic-implementation-in-react/) -- also, might be helpful as the approach can be used in a Vue environment – dhilt Mar 30 '20 at 11:12
1 Answers
16
While the concepts and references are mentioned in the comments to your question, here is my implementation for a simple Virtual Scroller in Vue.js
- Added comments everywhere so code is pretty self explanatory Supports Fixed item heights
- The idea is that we have a list of items displayed inside a div called spacer
- This spacer has a container div which keeps getting translatedY vertically called a viewport
- If each item was 30px in height and you wanted to show items 4 to 20, while hiding item 0 item 1 item2 and item 3, this viewport would be translated 120px vertically
- This viewport has a parent container called the root which only shows the subset of everything
See the WORKING VERSION HERE
HTML
<!-- https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib -->
<script type="text/x-template" id="virtual-scroll">
<div class="root" ref="root" :style="rootStyle">
<div class="viewport" ref="viewport" :style="viewportStyle">
<div class="spacer" ref="spacer" :style="spacerStyle">
<div v-for="item in visibleItems" :key="item">
{{item}}
</div>
</div>
</div>
</div>
</script>
<div id="app">
<header>
<h1>Vue.js Virtual Scroller</h1>
<h2>No Libraries Used</h2>
<h3>Keep Only a few items in DOM for a very large list</h3>
<p>Scroll below either by dragging the scroll bar or by moving your mouse wheel. Right Click any item in the list, click <b>Inspect Element</b> and check out the number of items in DOM, it is constant! Do you see how <b>smooth</b> it scrolls? Feel free to play with the number of items </p>
</header>
<virtual-scroll></virtual-scroll>
</div>
CSS
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
height: 100%;
}
body {
min-height: 100%;
height: 100%;
font-family: "Noto Sans", "Tahoma", sans-serif;
display: flex;
flex-direction: column;
color: rgba(0,0,0,0.6);
padding: 1.25rem;
}
header {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 1rem;
}
#app {
height: 100%;
}
.viewport {
background: #fefefe;
overflow-y: auto;
}
.spacer > div {
padding: 0.5rem 0rem;
border: 1px solid #f5f5f5;
}
Vue.js
// https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib
// define a mixin object
var passiveSupportMixin = {
methods: {
// This snippet is taken straight from https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
// It will only work on browser so if you are using in an SSR environment, keep your eyes open
doesBrowserSupportPassiveScroll() {
let passiveSupported = false;
try {
const options = {
get passive() {
// This function will be called when the browser
// attempts to access the passive property.
passiveSupported = true;
return false;
}
};
window.addEventListener("test", null, options);
window.removeEventListener("test", null, options);
} catch (err) {
passiveSupported = false;
}
return passiveSupported;
}
}
};
Vue.component("VirtualScroll", {
template: "#virtual-scroll",
mixins: [passiveSupportMixin],
data() {
return {
// A bunch of items with numbers from 1 to N, should be a props ideally
items: new Array(10000)
.fill(null)
.map((item, index) => "Item " + (index + 1)),
// Total height of the root which contains all the list items in px
rootHeight: 400,
// Height of each row, give it an initial value but this gets calculated dynamically on mounted
rowHeight: 30,
// Current scroll top position, we update this inside the scroll event handler
scrollTop: 0,
// Extra padding at the top and bottom so that the items transition smoothly
// Think of it as extra items just before the viewport starts and just after the viewport ends
nodePadding: 20
};
},
computed: {
/**
Total height of the viewport = number of items in the array x height of each item
*/
viewportHeight() {
return this.itemCount * this.rowHeight;
},
/**
Out of all the items in the massive array, we only render a subset of them
This is the starting index from which we show a few items
*/
startIndex() {
let startNode =
Math.floor(this.scrollTop / this.rowHeight) - this.nodePadding;
startNode = Math.max(0, startNode);
return startNode;
},
/**
This is the number of items we show after the starting index
If the array has a total 10000 items, we want to show items from say index 1049 till 1069
visible node count is that number 20 and starting index is 1049
*/
visibleNodeCount() {
let count =
Math.ceil(this.rootHeight / this.rowHeight) + 2 * this.nodePadding;
count = Math.min(this.itemCount - this.startIndex, count);
return count;
},
/**
Subset of items shown from the full array
*/
visibleItems() {
return this.items.slice(
this.startIndex,
this.startIndex + this.visibleNodeCount
);
},
itemCount() {
return this.items.length;
},
/**
The amount by which we need to translateY the items shown on the screen so that the scrollbar shows up correctly
*/
offsetY() {
return this.startIndex * this.rowHeight;
},
/**
This is the direct list container, we apply a translateY to this
*/
spacerStyle() {
return {
transform: "translateY(" + this.offsetY + "px)"
};
},
viewportStyle() {
return {
overflow: "hidden",
height: this.viewportHeight + "px",
position: "relative"
};
},
rootStyle() {
return {
height: this.rootHeight + "px",
overflow: "auto"
};
}
},
methods: {
handleScroll(event) {
this.scrollTop = this.$refs.root.scrollTop;
},
/**
Find the largest height amongst all the children
Remember each row has to be of the same height
I am working on the different height version
*/
calculateInitialRowHeight() {
const children = this.$refs.spacer.children;
let largestHeight = 0;
for (let i = 0; i < children.length; i++) {
if (children[i].offsetHeight > largestHeight) {
largestHeight = children[i].offsetHeight;
}
}
return largestHeight;
}
},
mounted() {
this.$refs.root.addEventListener(
"scroll",
this.handleScroll,
this.doesBrowserSupportPassiveScroll() ? { passive: true } : false
);
// Calculate that initial row height dynamically
const largestHeight = this.calculateInitialRowHeight();
this.rowHeight =
typeof largestHeight !== "undefined" && largestHeight !== null
? largestHeight
: 30;
},
destroyed() {
this.$refs.root.removeEventListener("scroll", this.handleScroll);
}
});
new Vue({
el: "#app"
});

PirateApp
- 5,433
- 4
- 57
- 90
-
3Original article. https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib – atilkan Nov 01 '20 at 17:22
-
1Great answer. I am trying to add a feature into this so that user can scroll to certain index. But unfortunately just changing offset is not working do you have any ideas to add this feature? – nicholasnet Feb 09 '21 at 00:52
-
@nicholasnet i dont understand your question, if the items have a fixed height say 50px each, the 10th item will be 500px from the top so where are you stuck? – PirateApp Aug 18 '21 at 13:47
-
1@PirateApp how about lazy load items a network call to load items from DB example? – Kamran Khatti Aug 15 '23 at 14:23