MeldUI

Use Case: Large Documents

Patterns for rendering 500+ page PDFs with bounded memory and responsive navigation.

DocumentViewer uses EmbedPDF’s tiling + viewport virtualization, so rendering a 1000-page PDF takes the same memory as rendering a 10-page PDF — only the pages currently in view are kept in memory.

What’s already optimized

These behaviours are on by default with no configuration needed:

  • Page virtualization — only pages in the visible viewport (plus a small overscan) are rendered to canvas
  • Tile virtualization — within each page, only tiles in the viewport are rendered; the rest are placeholder regions
  • Thumbnail lazy-loading — even with features.thumbnails on, thumbnails are generated as they scroll into view, not all at once
  • Worker mode — heavy operations (decode, search, render) run on a separate worker thread by default

Best practices

1. Use thumbnails for navigation

For 500+ page documents, scrolling end-to-end is slow even with virtualization. The thumbnail panel makes random-access navigation practical:

<DocumentViewer
  :features="{
    zoom: true,
    search: true,
    outline: true,
    thumbnails: true,
    download: true,
  }"
  ...
/>

2. Default to fit-width

fit-page for a 1000-page document means tiny pages. fit-width is usually better:

<DocumentViewer :feature-config="{ zoom: { defaultMode: 'fit-width' } }" ... />

3. Keep worker: true

The default worker={true} keeps the main thread responsive. Only opt out for the relative-wasmUrl issue (see Troubleshooting).

4. Surface a “loading” indicator

For PDFs larger than ~10 MB, the WASM decode + initial page render can take 1–3 seconds. Show a loading state while document-loaded hasn’t fired:

<script setup lang="ts">
import { ref } from 'vue'
import { DocumentViewer } from '@meldui/vue'

const loading = ref(true)
</script>

<template>
  <div class="relative h-[600px]">
    <div
      v-if="loading"
      class="absolute inset-0 z-10 flex items-center justify-center bg-background/80"
    >
      <Spinner />
    </div>
    <DocumentViewer
      source="/very-large.pdf"
      wasm-url="/pdfium.wasm"
      @document-loaded="loading = false"
      @document-error="loading = false"
    />
  </div>
</template>

5. Page-jump UI in your own toolbar

For documents where users routinely navigate to specific page numbers (legal contracts, statutes), add a “Go to page” input:

<script setup lang="ts">
const target = ref(1)
function go() {
  viewer.value?.goToPage(target.value)
}
</script>

<template>
  <DocumentViewer ref="viewer" ... />
  <input type="number" v-model.number="target" min="1" />
  <button @click="go">Go</button>
</template>

Memory expectations

Approximate steady-state memory for a typical PDF (8.5 × 11 inch, mixed text + images):

PagesMemory (Chrome, fit-width)
10~50 MB
100~80 MB
1000~120 MB
5000~180 MB

The constant ~50 MB is the WASM runtime + DOM. The growth is dominated by the document’s parsed structure (independent of how much you’ve scrolled).

Initial page

If a user lands on a deep-link to a specific page, set initial-page to avoid them scrolling there manually:

<DocumentViewer source="/long-doc.pdf" wasm-url="/pdfium.wasm" :initial-page="page" ... />

Documents with many annotations

If you’re loading 1000+ saved annotations on mount, loadAnnotations does the work in a single batch — no need to chunk.

For very high counts (10,000+ annotations across many pages), consider lazy-loading per page:

<DocumentViewer @page-change="onPageChange" />
async function onPageChange({ page }: { page: number }) {
  const range = [page - 1, page, page + 1] // page ± 1
  const annotations = await fetch(`/api/annotations?pages=${range.join(',')}`).then((r) => r.json())
  await viewer.value?.loadAnnotations(annotations)
}

See also