Skip to content

About

A svelte component to lazyload any content including images

Topics

Resources

Contributing

Stars

146 stars

Watchers

1 watching

Forks

Repository files navigation

icon

svelte-lazy

npm version CI

A Svelte component to lazy load any content: images, videos, components or anything else. It shows a placeholder until the content scrolls into view, and fades it in.

Native loading="lazy" covers images and iframes. svelte-lazy also defers rendering any markup or component, keeps the layout stable while images load, and works in scroll containers.

Demo: cards show a placeholder, then load as they scroll into view

Note

This is the README of 2.0, in prerelease: npm i svelte-lazy@next. It needs Svelte 5. For Svelte 3 or 4, use 1.x: npm i svelte-lazy@1, and see its README. To upgrade, see Migrating from 1.x.

Live examples (source) · Try it in the Svelte Playground

Install

npm i svelte-lazy

It needs Svelte 5, and comes with TypeScript types. For Svelte 3 or 4, use 1.x: npm i svelte-lazy@1.

Usage

<script>
  import Lazy from 'svelte-lazy';
</script>

<Lazy height={300} placeholder="Loading...">
  <img alt="" src="https://picsum.photos/id/412/400/300" />
</Lazy>

Important

Set height to about the height of the content. Before loading, the container has this height. With the default 0, all containers are empty and in view at once, so everything loads right away, and the page jumps when the content appears.

By default, content is unloaded when it leaves the viewport and loaded again when it comes back. Set keep to load it only once.

Examples

Load only once, with a snippet as the placeholder:

<script>
  import Lazy from 'svelte-lazy';
  import Spinner from './Spinner.svelte';
</script>

<Lazy height={400} keep>
  {#snippet placeholder()}
    <Spinner size="large" />
  {/snippet}
  <HeavyChart />
</Lazy>

Start loading earlier, 500px before the content scrolls into view, and know when it loads:

<Lazy height={300} offset={500} onload={(node) => console.log('loaded', node)}>
  <video controls src="movie.mp4"></video>
</Lazy>

Link to content that isn't loaded yet. The container has the id before loading, so #comments scrolls to it and loads it:

<a href="#comments">Comments</a>

<Lazy id="comments" height={600}>
  <Comments />
</Lazy>

Other attributes, like style, data-* and aria-*, go to the container:

<Lazy height={300} data-id={photo.id} aria-label={photo.name}>...</Lazy>

Style the container, the placeholder and the content by their classes:

<Lazy class="card" height={200}>...</Lazy>

<style>
  :global(.card .svelte-lazy-content) {
    border-radius: 8px;
  }
</style>

Props

Prop Type Default Description
height number | string 0 Height of the container before loading, in px or any CSS length like '50vh'
keep boolean false Keep the content after it leaves the viewport, instead of unloading it
offset number 150 Load when the top of the container is within this many px below the bottom of the viewport or the scroll container
placeholder string | Snippet null Shown before loading, and while images in the content are loading
id string undefined id of the container
class string '' Class added to the container, after svelte-lazy
fadeOption object | null { delay: 0, duration: 400 } Options of the fade transition. null to disable it
onload (node) => void null Called with the container element when the content is loaded
resetHeightDelay number 0 Delay in ms before the container height becomes auto after loading

How it works

  • The content loads when the container comes within offset of the bottom of the viewport, or of its scroll container, by scrolling or by a layout change. Unless keep is set, it unloads after leaving, and the container gets its height back. It's checked by IntersectionObserver, so nothing runs on scroll.
  • If the content has an image, the placeholder stays until the image loads, then the container height becomes auto. If the image fails, the content shows with the fixed height. Only the first image of the content is waited for: use resetHeightDelay to wait longer for others.
  • With server-side rendering, the server renders the placeholder; the content loads in the browser.
  • Only vertical scrolling is checked: in a horizontal scroll container, content loads by its vertical position.

The DOM before loading:

<div id="{id}" class="svelte-lazy {class}">
  <div class="svelte-lazy-placeholder">...</div>
</div>

and after:

<div id="{id}" class="svelte-lazy {class}">
  <div class="svelte-lazy-content">...</div>
</div>

Migrating from 1.x

  • 2.0 needs Svelte 5. With Svelte 3 or 4, stay on 1.x

  • A component placeholder becomes a snippet, and placeholderProps is gone:

    <!-- 1.x -->
    <Lazy placeholder={Spinner} placeholderProps={{ size: 'large' }}>...</Lazy>
    
    <!-- 2.0 -->
    <Lazy>
      {#snippet placeholder()}
        <Spinner size="large" />
      {/snippet}
      ...
    </Lazy>
  • The package has no prebuilt index.js and index.mjs. Bundlers that compile .svelte files from packages, like Vite and SvelteKit, work as before. Tools that don't, like Jest 27 or a script tag, need to, or stay on 1.x. For tests, see below

  • With TypeScript, other props are checked as attributes of the container div. placeholderProps, or a component as placeholder, is an error: change them as above

See the changelog for the other changes.

Tests

Tests in jsdom need two mocks, which a browser has:

  • IntersectionObserver, as with 1.x
  • Element.prototype.animate, for the fade, which Svelte 5 runs by Web Animations. Or set fadeOption={null}
// setup.js
Element.prototype.animate ??= function () {
  const animation = { onfinish: null, cancel() {}, finish() {}, play() {}, pause() {} };
  setTimeout(() => animation.onfinish?.());
  return animation;
};
Vitest

Vitest compiles .svelte files from packages by @sveltejs/vite-plugin-svelte. In jsdom, use Svelte's browser code:

// vitest.config.js
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte()],
  resolve: { conditions: ['browser'] },
  test: { environment: 'jsdom', setupFiles: ['./setup.js'] },
});

Vitest browser mode needs no mocks.

Jest

With Jest 29 and svelte-jester 5 in ESM mode, compile .svelte files from svelte-lazy, and from @testing-library/svelte-core if you use Testing Library:

// jest.config.js
export default {
  testEnvironment: 'jsdom',
  // svelte-lazy exports .svelte files by the svelte condition
  testEnvironmentOptions: { customExportConditions: ['svelte', 'browser'] },
  transform: { '^.+\\.svelte(\\.js)?$': 'svelte-jester' },
  transformIgnorePatterns: ['/node_modules/(?!(svelte-lazy|@testing-library/svelte-core)/)'],
  extensionsToTreatAsEsm: ['.svelte'],
  moduleFileExtensions: ['js', 'svelte'],
  setupFiles: ['./setup.js'],
};

and run it by NODE_OPTIONS=--experimental-vm-modules jest.

Changelog

See CHANGELOG.md.

Contributing

See CONTRIBUTING.md for development, tests and releases.

About

A svelte component to lazyload any content including images

Topics

Resources

Contributing

Stars

146 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages