DEV Community

Yu Wang
Yu Wang

Posted on • Edited on

Designing a Browser-Native File Preview Layer for Internal Web Apps

File Viewer began with a practical constraint at work. My manager asked me to build document preview entirely in the frontend because the backend had limited resources and the product served consumers at scale. I published the first article about the approach in 2022; I did not expect so many developers to adopt it or tell me it had solved a real problem for them.

The hardest part since then has been making many format engines behave consistently while keeping large customer files usable. I spent a lot of time on Worker loading, runtime asset delivery, and performance so that heavy parsers remain practical outside a demo.

File Viewer v2.2.2 showing multiple file formats entirely in the browser

The current English demo routes 206 extensions through 24 lazy browser-side preview pipelines.

Adding file preview to a web application often looks simple at first. Display images with an <img> element, embed PDFs, and download everything else.

That approach usually stops working when an internal application accumulates real business attachments: DOCX reports, XLSX workbooks, PPTX decks, legacy Office files, CAD drawings, archives, email messages, diagrams, source code, and structured data. Teams then end up operating a conversion backend, sending files to an external service, or maintaining a collection of unrelated viewers.

File Viewer is an Apache-2.0 project that treats this problem as frontend infrastructure. It provides a common browser-side loading, rendering, interaction, and deployment layer while keeping format-specific engines modular.

This article explains the architectural choices behind it, including the tradeoffs and the places where browser-native preview is not the right answer.

Start With Deployment Constraints

The project was designed around internal tools, intranet systems, approval workflows, attachment centers, engineering archives, and private deployments.

These environments frequently have constraints that are easy to overlook:

  • Documents may not be uploaded to a third-party converter.
  • The application may run without public internet access.
  • Worker, WASM, font, and vendor assets must come from an approved internal origin.
  • A strict Content Security Policy may block dynamically created Workers or remote resources.
  • The host application may use Vue, React, Svelte, jQuery, or no framework at all.
  • A global design system may contain aggressive CSS resets.
  • Most users open common files, but a small number need much heavier CAD or archive engines.

Those constraints make a single monolithic viewer bundle unattractive. They also make “just run a document conversion server” an incomplete answer.

Separate Orchestration From Rendering

The project is divided into several layers.

@file-viewer/core owns the behavior shared by every format:

  • source loading and format detection;
  • renderer registration and selection;
  • lifecycle and controller APIs;
  • search and highlight state;
  • zoom and navigation state;
  • print and HTML export coordination;
  • download and operation guards;
  • theme, watermark, and toolbar configuration.

Format-specific behavior lives in @file-viewer/renderer-* packages. PDF, Word, spreadsheets, presentations, CAD, archives, email, drawings, geospatial data, and other families can therefore evolve independently.

This boundary is important. The core should not need to understand how a DWG file is decoded or how a ZIP entry is extracted. It only needs a renderer contract that describes whether the active document supports operations such as search, print, zoom, or export.

That also prevents the common toolbar from advertising operations a renderer cannot perform reliably. For example, a PDF renderer can provide full-page printing, while an archive browser may expose nested preview and extraction but intentionally hide print.

Assemble Capabilities With Presets

Most applications do not need every supported format. File Viewer groups common renderer sets into presets:

  • @file-viewer/preset-lite covers text, Markdown, code, images, audio, and video.
  • @file-viewer/preset-office covers PDF, Word, Excel, PowerPoint, OFD, RTF, and OpenDocument workflows.
  • @file-viewer/preset-engineering covers CAD, 3D, geospatial data, diagrams, archives, and engineering formats.
  • @file-viewer/preset-all provides the capability set used by the complete demo.

An application can also install one renderer directly when it only needs an exact format boundary.

The *-full framework packages favor fast setup by combining a component package with the full preset. They are useful for evaluation, attachment workbenches, and applications where broad coverage matters more than the smallest possible dependency graph.

Production applications with a narrower format set should usually start with a native component package and add only the required preset or renderers.

Lazy Loading Is More Important Than the Extension Count

At the time of writing, the registry declares 206 extension mappings across 24 preview pipelines. That does not mean all 206 implementations are downloaded when the page starts.

Heavy Office, PDF, CAD, archive, Typst, drawing, PSD, Worker, and WASM paths load when their file types are selected. A user opening Markdown should not first download a CAD decoder.

The CDN full entry follows the same idea. Its initial script installs the Custom Element, controller, and lazy capability registry. Renderer bundles are fetched from the distribution directory when needed. Worker, WASM, font, and vendor assets can be served from the same origin or copied to a private asset path.

For Vite applications, an optional plugin can discover installed presets and copy their runtime assets. Other build systems can pass presets explicitly and copy assets as part of their normal deployment process.

This architecture does not make heavy document engines small. It limits when they are transferred and lets the application exclude engines it never uses. Bundle-size claims should therefore be based on the selected preset and measured production build, not on one universal number.

Keep Framework Integrations Thin

The same core is exposed through packages for Web Components, Vanilla JavaScript, Vue 3, Vue 2.7/2.6, React, legacy React, Svelte, and jQuery.

Framework packages translate native concepts—props, events, refs, hooks, actions, or plugins—into the same viewer options and controller semantics. They do not maintain separate rendering implementations.

A basic React integration using the full package looks like this:

import FileViewer from '@file-viewer/react-full'

export function Preview() {
  return (
    <FileViewer
      url="/files/report.pdf"
      style={{ height: 720 }}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

A no-build page can use the Web Component entry:

<script src="https://cdn.jsdelivr.net/npm/@file-viewer/web-full@latest/dist/flyfish-file-viewer-web-full.iife.js"></script>

<flyfish-file-viewer
  src="/files/report.docx"
  theme="light"
  style="display:block;height:720px">
</flyfish-file-viewer>
Enter fullscreen mode Exit fullscreen mode

The Web Component and IIFE paths use Shadow DOM isolation by default. Framework integrations retain light-DOM compatibility but can opt into stronger isolation. CSS variables and stable Shadow Parts provide safer customization than depending on internal class names.

Be Precise About Privacy

“Browser-side” should not be translated into “the application never makes a network request.”

File Viewer does not require a server-side document conversion service. A local file can be parsed and rendered in the browser, and runtime assets can be self-hosted. This means a private document does not need to be sent to a third-party SaaS converter.

The final privacy boundary still depends on deployment:

  • A remote document URL must be fetched from somewhere.
  • Worker, WASM, fonts, and vendor scripts use the origins configured by the application.
  • Optional integrations, such as a configured PlantUML rendering endpoint, can introduce another request.
  • Application telemetry remains the host application’s responsibility.

For an offline or intranet deployment, teams should self-host the complete runtime asset set, audit configured endpoints, and test under the production CSP.

Extension Support Is Not a Fidelity Guarantee

A long extension list can become misleading if every recognized file is described as fully rendered.

The project documents several capability levels. Mature pipelines provide visual preview and useful document operations. Some specialized engineering formats currently provide safe structure inspection, metadata, or conversion guidance. Extremely complex formats may require a dedicated C++, Rust, or WASM engine before high-fidelity rendering is realistic.

Office preview also has a different goal from running Microsoft Office itself. Complex pagination, fonts, SmartArt, embedded objects, macros, and vendor-specific layout behavior can produce differences. The correct evaluation method is to test representative, sanitized production files and review the published fidelity notes.

The component is also read-only. Download preserves the original bytes; it does not write rendered changes back into the source document. Applications needing collaborative editing, tracked changes, or Office-compatible authoring should evaluate a document editing suite instead.

Self-Hosting and Evaluation

The project provides npm packages, static release artifacts, and a Docker image. A local demo can be started with:

docker run --rm -p 8080:80 flyfishdev/file-viewer:latest
Enter fullscreen mode Exit fullscreen mode

For evaluation, start with the live samples, then test sanitized files that resemble production data. Verify rendering, search, print, asset loading, mobile behavior, and CSP rules before selecting a preset.

Browser-native preview is a good fit when an application needs read-only access to mixed attachments, private deployment, frontend integration, and a consistent interaction layer. It is a weaker fit when exact Office editing, server-generated canonical output, or professional CAD authoring is the main requirement.

The useful question is not whether one viewer can replace every native application. It is whether a modular browser preview layer can remove enough downloads, conversion jobs, and one-off integrations to improve a real workflow.

Repository: https://github.com/flyfish-dev/file-viewer

English demo: https://demo.file-viewer.app/?lang=en-US

Documentation: https://doc.file-viewer.app/en/

Format notes: https://doc.file-viewer.app/en/guide/format-fidelity

Top comments (0)