Lazy Loading
By default the JavaScript for every custom tag used on a page is included in that page's bundle. The load import attribute instead splits the tag into its own bundle, which is fetched in the browser only after a trigger has fired.
import VideoPlayer from "<video-player>" with { load: "visible#hero" };
<section id="hero">
<VideoPlayer src=input.src/>
</section>import VideoPlayer from "<video-player>" with { load: "visible#hero" }
<section#hero>
<VideoPlayer src=input.src/>
</section>import VideoPlayer from "<video-player>" with { load: "visible#hero" };
section id="hero"
VideoPlayer src=input.srcimport VideoPlayer from "<video-player>" with { load: "visible#hero" }
section#hero
VideoPlayer src=input.srcWhen rendered on the server, a lazily loaded tag writes its HTML immediately like any other tag. Only its client-side JavaScript is deferred, and the tag becomes interactive once the trigger has fired and its bundle has loaded.
When rendered in the browser, nothing is displayed in the tag's place until the trigger has fired and its bundle has loaded. The <try> tag can be used to show placeholder content in the meantime.
Attributes passed to a lazily loaded tag remain reactive while it loads. Once its JavaScript arrives, the tag picks up the latest values.
The load attribute may only be used when importing a custom tag as a default import. It works with both relative paths and the tag import shorthand.
Triggers
The value of the load attribute is either "render" or one or more triggers which determine when the tag's JavaScript is loaded.
Most triggers accept a CSS selector for an element to watch, and some accept additional options using a query string syntax.
If a trigger's selector does not match any element on the page, the tag's JavaScript is loaded immediately (with a warning in development).
render
Loads the tag's JavaScript as soon as the tag is rendered.
import MarkdownEditor from "<markdown-editor>" with { load: "render" };
<MarkdownEditor value=input.draft/>import MarkdownEditor from "<markdown-editor>" with { load: "render" }
<MarkdownEditor value=input.draft/>import MarkdownEditor from "<markdown-editor>" with { load: "render" };
MarkdownEditor value=input.draftimport MarkdownEditor from "<markdown-editor>" with { load: "render" }
MarkdownEditor value=input.draftThe render trigger does not wait for any user interaction. It splits the tag into its own bundle that is only loaded on pages which actually render the tag, which is useful for tags that are rendered conditionally or are heavy enough to be worth splitting out of the main bundle.
The render trigger must be used alone and cannot be combined with multiple triggers.
visible
Loads the tag's JavaScript once the element matching the selector becomes visible in the viewport (via an IntersectionObserver).
import Comments from "<comments>" with { load: "visible#comments" };
<section id="comments">
<Comments post=input.post/>
</section>import Comments from "<comments>" with { load: "visible#comments" }
<section#comments>
<Comments post=input.post/>
</section>import Comments from "<comments>" with { load: "visible#comments" };
section id="comments"
Comments post=input.postimport Comments from "<comments>" with { load: "visible#comments" }
section#comments
Comments post=input.postThe observer's rootMargin may be configured to begin loading before the element actually enters the viewport.
import Comments from "<comments>" with { load: "visible#comments?rootMargin=100px" };import Comments from "<comments>" with { load: "visible#comments?rootMargin=100px" }import Comments from "<comments>" with { load: "visible#comments?rootMargin=100px" };import Comments from "<comments>" with { load: "visible#comments?rootMargin=100px" }idle
Loads the tag's JavaScript when the browser is idle (via requestIdleCallback). In browsers without requestIdleCallback, loading begins immediately.
import Recommendations from "<recommendations>" with { load: "idle" };
<Recommendations/>import Recommendations from "<recommendations>" with { load: "idle" }
<Recommendations/>import Recommendations from "<recommendations>" with { load: "idle" };
Recommendationsimport Recommendations from "<recommendations>" with { load: "idle" }
RecommendationsA timeout (in milliseconds) may be provided to ensure loading begins even if the browser never becomes idle.
import Recommendations from "<recommendations>" with { load: "idle?timeout=2000" };import Recommendations from "<recommendations>" with { load: "idle?timeout=2000" }import Recommendations from "<recommendations>" with { load: "idle?timeout=2000" };import Recommendations from "<recommendations>" with { load: "idle?timeout=2000" }The idle trigger does not accept a selector.
media
Loads the tag's JavaScript once the media query in parentheses matches. Loading begins immediately if the query already matches, otherwise as soon as it first does.
import MobileNav from "<mobile-nav>" with { load: "media(max-width: 768px)" };
<MobileNav/>import MobileNav from "<mobile-nav>" with { load: "media(max-width: 768px)" }
<MobileNav/>import MobileNav from "<mobile-nav>" with { load: "media(max-width: 768px)" };
MobileNavimport MobileNav from "<mobile-nav>" with { load: "media(max-width: 768px)" }
MobileNavThis pairs well with tags hidden by CSS breakpoints, so that (for example) desktop users never download mobile-only UI.
Events
A trigger beginning with on loads the tag's JavaScript the first time the matching event fires on the element matching the selector.
import EmojiPicker from "<emoji-picker>" with { load: "on-focus#message" };
<input placeholder="Say something nice" id="message">
<EmojiPicker/>import EmojiPicker from "<emoji-picker>" with { load: "on-focus#message" }
<input#message placeholder="Say something nice">
<EmojiPicker/>import EmojiPicker from "<emoji-picker>" with { load: "on-focus#message" };
input placeholder="Say something nice" id="message"
EmojiPickerimport EmojiPicker from "<emoji-picker>" with { load: "on-focus#message" }
input#message placeholder="Say something nice"
EmojiPickerThe event name follows the same casing rules as event handler attributes: with on- the event name casing is preserved, otherwise the event name is all lowercased (so on-click and onClick are equivalent).
Multiple Triggers
Multiple triggers may be combined with |. The tag's JavaScript is loaded when the first of them fires.
import ChatWidget from "<chat-widget>" with { load: "on-mouseover#chat | idle?timeout=5000" };
<button id="chat">Chat with us</button>
<ChatWidget/>import ChatWidget from "<chat-widget>" with { load: "on-mouseover#chat | idle?timeout=5000" }
<button#chat>Chat with us</button>
<ChatWidget/>import ChatWidget from "<chat-widget>" with { load: "on-mouseover#chat | idle?timeout=5000" };
button id="chat" -- Chat with us
ChatWidgetimport ChatWidget from "<chat-widget>" with { load: "on-mouseover#chat | idle?timeout=5000" }
button#chat -- Chat with us
ChatWidgetPlaceholders & Errors
In the browser, a lazily loaded tag behaves like async content: while its JavaScript is loading, a <try> ancestor displays its @placeholder content, and if loading fails the error is handled by the nearest @catch.
import Comments from "<comments>" with { load: "on-click#show-comments" };
<let/show=false>
<button onClick() {
show = true;
} id="show-comments">Show Comments</button>
<try>
<if=show>
<Comments post=input.post/>
</if>
<@placeholder>Loading comments...</@placeholder>
<@catch|err|>Failed to load comments: ${err.message}</@catch>
</try>import Comments from "<comments>" with { load: "on-click#show-comments" }
<let/show=false>
<button#show-comments onClick() { show = true }>Show Comments</button>
<try>
<if=show>
<Comments post=input.post/>
</if>
<@placeholder>Loading comments...</@placeholder>
<@catch|err|>Failed to load comments: ${err.message}</@catch>
</try>import Comments from "<comments>" with { load: "on-click#show-comments" };
let/show=false
button onClick() {
show = true;
} id="show-comments" -- Show Comments
try
if=show
Comments post=input.post
@placeholder -- Loading comments...
@catch|err| -- Failed to load comments: ${err.message}import Comments from "<comments>" with { load: "on-click#show-comments" }
let/show=false
button#show-comments onClick() { show = true } -- Show Comments
try
if=show
Comments post=input.post
@placeholder -- Loading comments...
@catch|err| -- Failed to load comments: ${err.message}Bundler Support
Lazy loading is coordinated with the bundler through the linkAssets compiler option. @marko/vite (and therefore Marko Run) configures this automatically, so no setup is required.
Contributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.