npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue-viewport-render

v0.1.0

Published

Vue 3 component that mounts content on viewport entry and exposes live visibility, so off-screen work can be gated at the data source.

Readme

vue-viewport-render

A Vue 3 component that defers mounting until its slot enters the viewport, and keeps publishing live visibility afterwards so descendants can stop doing work while off-screen.

Zero runtime dependencies. Only vue as a peer.

Why

Two separate problems get conflated under "lazy render":

  1. Don't mount what nobody has seen yet. IntersectionObserver solves this directly.
  2. Don't keep recomputing what nobody is looking at. This one cannot be solved by stopping a parent from re-rendering — every descendant component owns its own reactive effect, so a grandchild subscribed to a store keeps re-rendering no matter what its parent does. It has to be solved at the data source, and for that the component needs to tell its descendants whether it is on screen.

LazyRender handles the first and gives you the hook for the second.

Installation

npm install vue-viewport-render

Usage

<script setup>
import { LazyRender } from 'vue-viewport-render'
</script>

<template>
  <LazyRender tag="section" class="chart-slot" root-margin="200px">
    <HeavyChart />
    <template #fallback>
      <ChartSkeleton :height="320" />
    </template>
  </LazyRender>
</template>

The wrapper element stays in the DOM after activation, so tag, class, style and any other fallthrough attribute keep applying, and the wrapper is where you reserve height to avoid layout shift:

<LazyRender style="min-height: 320px"><HeavyChart /></LazyRender>

Gating work while off-screen

Read visibility from any depth and gate the subscription itself:

<script setup>
import { shallowRef, watchEffect } from 'vue'
import { useLazyRenderVisible } from 'vue-viewport-render'
import { store } from './store'

const visible = useLazyRenderVisible()
const chart = shallowRef()

watchEffect((onCleanup) => {
  if (!visible.value) return

  const redraw = () => chart.value?.render(store.data)
  redraw()
  const off = store.onChange(redraw)
  onCleanup(off)
})
</script>

The same check stops polling, requestAnimationFrame loops, and video playback. A scoped slot prop is available if you prefer not to use inject:

<LazyRender>
  <template #default="{ isVisible }">
    <HeavyChart :active="isVisible" />
  </template>
</LazyRender>

Scroll containers

root accepts an element, a ref to one, a component instance, or a getter returning any of those. It is reactive — the observer is rebuilt when the root changes, including the common case where a parent template ref is still null while the child sets up:

<script setup>
import { ref } from 'vue'
const scroller = ref(null)
</script>

<template>
  <div ref="scroller" class="scroller">
    <LazyRender :root="scroller" root-margin="100px"><Panel /></LazyRender>
  </div>
</template>

Mount-only mode

When you only want deferral and not off-screen gating — a long feed of cards, for instance — set once so the observer is disconnected as soon as the content activates:

<LazyRender once><FeedCard /></LazyRender>

Note that once and useLazyRenderVisible are mutually exclusive: with the observer gone, visibility is frozen at true.

API

LazyRender props

| Prop | Type | Default | Notes | | --- | --- | --- | --- | | tag | string | 'div' | Wrapper element. Persists after activation. | | root | Element \| Ref<Element> \| Component \| (() => Element) | null | Observer root. null means the browser viewport. Reactive. | | rootMargin | string | — | CSS margin syntax. Reactive. | | threshold | number \| number[] | 0 | Read once at setup, deliberately not reactive — an inline array literal in a template is a new reference on every render and would rebuild the observer each time. | | once | boolean | false | Disconnect the observer after first activation. |

Events

| Event | Payload | Notes | | --- | --- | --- | | change | boolean | Fires only when visibility actually flips, so multiple thresholds do not produce duplicate events. |

Slots

| Slot | Props | Notes | | --- | --- | --- | | default | { isVisible, activated } | Rendered from the first activation onwards, and never swapped back to the fallback. | | fallback | — | Rendered before the first activation. |

Exposed

isVisible and activated, both refs, via a component template ref.

useLazyRenderVisible()

Returns a readonly Ref<boolean> — the live visibility of the nearest LazyRender ancestor, or true when called outside one.

useIntersectionVisibility(target, options)

The composable underneath, exported for building your own deferral logic.

| Option | Type | Notes | | --- | --- | --- | | root | see above | Reactive. | | rootMargin | string \| Ref<string> \| (() => string) | Reactive. | | threshold | number \| number[] | Read once. | | onChange | (isVisible: boolean) => void | Called only on actual flips. |

Returns { isVisible, isSupported, stop }. isSupported is false where IntersectionObserver is unavailable. Cleanup is automatic on effect scope disposal.

Behaviour notes

SSR. The server and the client's first frame both render fallback, so hydration matches. Nothing crashes on the server. The trade-off is that crawlers receive the skeleton, so do not wrap SEO-critical content. When IntersectionObserver is unavailable in the browser, content is released on mount rather than never appearing.

No unmounting. Once activated, content stays mounted. This defers work; it does not reclaim memory. For long lists use a virtual scroller.

Layout shift. Nothing reserves space for you. Set a height on the wrapper or on the fallback content.

License

MIT