3490 views
2021-12-22
Lazy loading is a useful technique that can delay loading non-essential content in an application until it is needed. It can reduce the bundle size and increase the loading performance. However, there are many implications and solutions of that in history.
In history, existed a number of implementations of lazy loading, visibility detection and identifying the relationship between two elements. Different events played important roles. It always came with the performance burden so that developers invent various methods to fix the performance impact, such as throttle and debounce.
iOS UIWebViews only invoke the scrol event when the scrolling has completed MDN
// simple logic of the scroll event handler for bottom loading
function lazyLoad(e) {
// when the distance to list bottom is small, load next page of list
if (e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight) {
// it may be called multiple times
loadNextPage()
}
}
Scroll event will be called much time during scrolling, it absolutely affects the performance. To reduce the performance influence of DOM mounting, another performance problem is brought.
To solve the performance issue of the high-frequency events, some developers think that we can develop a lock of event handling. Just like a vendor machine, no matter how many times you press the button, the next task will not proceed until the task is completed.
// with throttle function, the lazyLoad will only be fired one time within 0.5s
function throttle(callback, wait) {
let waiting = false;
return function () {
if (waiting) {
return
}
waiting = true;
setTimeout(() => {
callback.apply(this, arguments);
waiting = false;
}, wait)
}
}
const lazyLoad = debounce(() => {
...
}, 500)
Smart developer also think of another way to handle the problem, develop a buffer with setTimeout or Date. BTW, this function was widely used to handle input event.
// with debounce, all the loading function will only be fired once after the page stops scrolling.
function debounce(callback, wait) {
let timer
return function() {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
callback.apply(this, arguments)
}, wait)
}
}
const lazyLoad = debounce(() => {
...
}, 500)
The solution above was still written by JavaScript or limited by no direct solution. With the development of the web browser, some new experimental APIs was released to be supposed to solve these problems.
Intersection Observer API is one of the solutions to solve the problem. The difficulty of implementing this function is solving the loop calling and the thread scheduling. Imagine when you are developing an infinite scrolling page, every detection, UI rendering and every other intersection run on the main thread. Intel and Qualcomm can contribute more to help JavaScript to warm the Earth.
The Intersection Observer API can register a callback function that will be executed when elements are entering, displaying or intersecting. There are no code that will run on the main thread.
Intersection Observer can not reflect the exact number of pixels that overlap.
With the definition of document, the callback will be triggered when:
It is a bit obscure, so we start from the usage.
const blogs = document.querySelectorAll('.blog')
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
// do something to change the UI
})
}, {
threshold: 0.3,
rootMargin: "-20px"
})
blogs.forEach(blog => {
observer.observe(blog)
})
The usage is very clear, only two step:
With the document of MDN, IntersectionObserver receive two parameters:
root must be the ancestor of the target element
threshold can be an array likes
[0, 0.25, 0.5, 0.75, 1]
to specify the executed times.
The callback will be fired when the minimal rectangle of one of the elements is displayed or disappear. When callback is executed, it will return IntersectionObserverEntry array that include the entry objects relative to the observed elements.
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
Normally, the isIntersecting variable need to be checked to find elements that are currently visible with the root.
This callback is running on the main thread, so the operation should be quickly. The time-consuming function should use window.requestIdleCallback()
This option receives a numeric value of an array of numbers. When the intersection of the target reaches this threshold, the observer will fire the callback. You can set a numeric array to execute the callback multiple times. 0.5 equal to 50% of the target's width/height is the thresholds.
This option receives a DOM element which is supposed to be the parental or ancestral element of the target. Its default value is browser viewport.
In some browsers, the parameter cannot be a Document.
This option is the margin between the root element and the observed actual viewport. value is similar to the CSS margin. The number of pixels should follow the top-right-bottom-left rule. Also, the value can be percentages. Defaults to all zero.
Let's use this API to create a simple lazy loading list.
// Posit the all items of the list have clas name item, the list has the class named list
const list = document.querySelectorAll('.item')
const listObserver = new IntersectionObserver(entries => {
const lastItem = entries[0]
if(!lastItem.isIntersection) return
// Call loading API and render new items
loadNextPage()
// The last item has been changed
listObserver.unobserve(lastItem.target)
listObserver.observe(document.querySelectorAll('.list:last-child'))
})
listObserver.observe(document.querySelectorAll('.list:last-child'))
It was pretty handy, you can now update your website. Let's to more measures to eliminate IE.