"One weird trick" we use for boosting performance of elements added to the DOM, is conditional rendering. We'll cover a few different methods we use for conditional / lazy rendering, first starting with a mixin approach.
If you're not aware of what an IntersectionObserver is, it's basically a low level javascript API that allows us to generate an event when something is visible. This means that you can respond to the event of the user scrolling down in your app and "seeing" your element. By knowing when our element is visible, we can conditionally load... the whole the internals of the element for optimizing performance!
While incentivizing usage of LitElement, IntersectionObserverMixin is a vanillaJS mixin that works with any web component. Let's look at what it does first and then different examples, all of which are more or less the same.
TLDWTRTC (Too long, don't want to read the code)
- We export a SuperClass which supports a
new IntersectionObserver
- This has some reasonable defaults applied in
constructor
which can be changed in your implementation -
connectedCallback
we activate the observer -
disconnectedCallback
we disconnect the observer - LitElement specific: We have a reactive property called
elementVisible
Show me the code (or skip if lazy-loading your memory)
const IntersectionObserverMixin = function (SuperClass) {
// SuperClass so we can write any web component library / base class
return class extends SuperClass {
/**
* Constructor
*/
constructor() {
super();
// listen for this to be true in your element
this.elementVisible = false;
// threasholds to check for, every 25%
this.IOThresholds = [0.0, 0.25, 0.5, 0.75, 1.0];
// margin from root element
this.IORootMargin = "0px";
// wait till at least 50% of the item is visible to claim visible
this.IOVisibleLimit = 0.5;
// drop the observer once we are visible
this.IORemoveOnVisible = true;
// delay in observing, performance reasons for minimum at 100
this.IODelay = 100;
}
/**
* Properties, LitElement format
*/
static get properties() {
let props = {};
if (super.properties) {
props = super.properties;
}
return {
...props,
elementVisible: {
type: Boolean,
attribute: "element-visible",
reflect: true,
},
};
}
/**
* HTMLElement specification
*/
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
}
// setup the intersection observer, only if we are not visible
if (!this.elementVisible) {
this.intersectionObserver = new IntersectionObserver(
this.handleIntersectionCallback.bind(this),
{
root: document.rootElement,
rootMargin: this.IORootMargin,
threshold: this.IOThresholds,
delay: this.IODelay,
}
);
this.intersectionObserver.observe(this);
}
}
/**
* HTMLElement specification
*/
disconnectedCallback() {
// if we have an intersection observer, disconnect it
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
}
/**
* Very basic IntersectionObserver callback which will set elementVisible to true
*/
handleIntersectionCallback(entries) {
for (let entry of entries) {
let ratio = Number(entry.intersectionRatio).toFixed(2);
// ensure ratio is higher than our limit before trigger visibility
if (ratio >= this.IOVisibleLimit) {
this.elementVisible = true;
// remove the observer if we've reached our target of being visible
if (this.IORemoveOnVisible) {
this.intersectionObserver.disconnect();
}
}
}
}
};
};
export { IntersectionObserverMixin };
Usage in practice
So when we leverage this in production we get timing as follows:
- Element definition loads (usually with dynamic
import()
from previous post) -
render()
of element uses a ternary statement (LitElement specific) in order to conditionally render the contents of the element${this.elementVisible ? html
render my element shadow html:
render nothing by default}
- When
${this.elementVisibile}
changes to true we apply aupdated()
life-cycle callback (LitElement specific) in order to do additional dynamicimport()
s of internal elements used in the shadow of our element - When the element is visible, it will disconnect the IntersectionObserver by default
This approach maximizes performance with minimal effort. It also allows for content authors in HAX to add elements to the page like video-player
or wikipedia-query
or a11y-gif-player
and not have to think about performance yet obtain reasonably high scores for CMS driven, user generated content.
What implementation looks like
Basically two or three things and you've got a faster element with a single dependency. These examples are from wikipedia-query
- import it:
import { IntersectionObserverMixin } from "@lrnwebcomponents/intersection-element/lib/IntersectionObserverMixin.js";
- Conditionally render:
// LitElement render function
render() {
return html` ${this.elementVisible
? html` <h3 .hidden="${this.hideTitle}" part="heading-3">
${this._title}
</h3>
<div id="result"></div>
<citation-element
creator="{Wikipedia contributors}"
scope="sibling"
license="by-sa"
title="${this.search} --- {Wikipedia}{,} The Free Encyclopedia"
source="https://${this
.language}.wikipedia.org/w/index.php?title=${this.search}"
date="${this.__now}"
></citation-element>`
: ``}`;
}
- Conditionally import / use the variable for conditional logic internally:
updated(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
// element is visible, now we can search
if (propName == "elementVisible" && this[propName]) {
import("@lrnwebcomponents/citation-element/citation-element.js");
}
if (
["elementVisible", "search", "headers", "language"].includes(
propName
) &&
this.search &&
this.headers &&
this.elementVisible &&
this.language
) {
clearTimeout(this._debounce);
this._debounce = setTimeout(() => {
this.updateArticle(this.search, this.headers, this.language);
}, 10);
}
});
}
Here we can see an additional conditional logic for wikipedia-query
in that it uses elementVisibile
along with having a search
term, language
, and headers
in order to trigger a fetch()
of the wikipedia public API. This means that if you have a wikipedia article embedded lower in your website, that your user won't invoke the API to load the article until it's in the viewport!
Some other code examples where we've implemented this IntersectionObserverMixin to juice performance on an element:
This can be used with VanillaJS implementations as well by observering the attribute for changes, though no implementations exist here. You can also find a more advanced implementation in lazyImageLoader which is a SuperClass that mixes in IntersectionObserverMixin to do a lazyloading image placeholder SVG.
Top comments (0)