Atif Afzal

Are web apps really slower than native?

TL;DR

Web apps aren't slower than native apps. The web platform makes the performant path harder to reach than it should be. Native platforms bake performance into defaults (recycler views, transitions, spring animations); the web has equivalents for all of these, but they're scattered across niche libraries and require expert knowledge. For 90% of apps that are CRUD over REST, the DOM is more than fast enough. For the rest, Figma, Photoshop, and Google Sheets prove the ceiling is high. The gap is closing fast.


The default path on iOS is already performant. The default path on the web is the janky path. That's not a speed problem, it's a defaults problem.

For most apps, it doesn't matter

90% of web apps are forms, lists, filters, and detail views. CRUD over REST. The DOM handles this fine. A <ul> with 50 items, a form with validation, a table with sorting and pagination: none of these are anywhere near the DOM's performance ceiling.

The "web is slow" discourse is driven by the 10% of apps trying to be Figma, applied as a blanket judgment on the entire platform. Most web apps are not rendering 100,000 objects on a canvas or processing megabytes of image data. They're fetching JSON and showing it.

For the rest, the web can keep up

Figma is a professional design tool that competes with Sketch and Photoshop. It runs in the browser. The rendering engine is C++ compiled to WebAssembly, and Figma was one of the first production apps to migrate from WebGL to WebGPU for its rendering pipeline. [1]

Adobe ported Photoshop to the browser using 80MB+ of WebAssembly modules. SIMD instructions give 3-4x speedups on average for image processing, and up to 160x for specific operations via Halide. [2]

Google Sheets compiled its calculation engine to WasmGC and got 2x faster than the previous JavaScript implementation. [3]

These aren't toy demos. They're production apps used by millions of people, running in a browser tab, competing with their native counterparts. For compute-heavy workloads, WebAssembly runs within striking distance of native C++ — and browser engines keep closing the gap. [4] The technology is not the bottleneck.

Why web apps feel slower

Native bakes it in, web makes you opt in

The pattern is the same across every layer of the stack: native platforms make the performant path the default, the web makes you opt in.

Lists

On iOS, UICollectionView recycles cells automatically. You get a performant scrolling list by default. On Android, RecyclerView does the same. On the web, there is no browser-native virtualizing list. You render 10,000 DOM nodes, it gets slow, and then you go looking for react-window or tanstack-virtual. Each library has its own API, its own tradeoffs, its own quirks with accessibility and find-in-page.

The platform is catching up here. content-visibility: auto tells the browser to skip rendering for off-screen elements entirely, no layout, no paint, while preserving accessibility and find-in-page that JavaScript virtualizers break (in Chromium; Safari doesn't yet search hidden content). Baseline across all browsers since September 2024. [5]

Node.moveBefore() enables state-preserving DOM moves (animations keep playing, focus is retained, iframes don't reload), making recycler-view-like patterns possible without losing state. Baseline since June 2025. [6]

Route transitions

iOS and Android have built-in navigation transitions: push, modal, shared element. You get them for free when you use the platform's navigation stack. On the web, most apps do a hard cut between routes.

The View Transitions API Level 2 changes this. It enables cross-document page transitions with CSS-driven animations between routes, the single biggest "this feels like a native app" feature the web has shipped. It's in Chrome and Safari now. [7] The gap here is closing fast, but most web developers haven't adopted it yet.

Animations

On iOS, UIView.animate(withDuration:) gives you spring physics, interruptible animations, and compositor-thread performance by default. On the web, getting equivalent results means knowing which properties run on the compositor thread (transform, opacity, and more recently background-color and clip-path [8]). [9] It means choosing between CSS animations, the Web Animations API, Framer Motion, GSAP, or a dozen other libraries. The knowledge barrier is high enough that most web developers don't animate at all, or animate badly.

Scroll-Driven Animations are a step in the right direction: parallax, reveal-on-scroll, and progress indicators in pure CSS with no JavaScript, driven by scroll position instead of timers. [10] CSS @starting-style enables entry animations for elements appearing in the DOM without JavaScript. These lower the barrier, but the web still doesn't have a single, obvious "animate this" API that matches native's simplicity.

Scrolling

On native platforms, scroll runs on a separate thread. It's always 60fps regardless of what the main thread is doing. On the web, a single non-passive touchstart listener can block scrolling entirely while JavaScript runs.

This was such a widespread problem that Chrome had to retroactively change the default for addEventListener on document-level touch events to { passive: true }. The old default, the one every developer was using, was the slow path. The platform had to patch itself because its own default was causing jank.

Back navigation

On iOS, swiping back preserves everything: scroll position, form inputs, animation state. The view controller stays in memory. On the web, pressing back often re-renders the entire page. The browser's bfcache is supposed to help, but single-page app frameworks frequently break it with unload listeners or cache-busting headers. Preserving state across navigations requires explicit work that native gives you for free.

The Navigation API (baseline January 2026) gives SPAs browser-native routing with navigation.navigate() and event interception, replacing the brittle history.pushState() pattern. [11] Combined with bfcache-aware coding (avoiding unload, using pagehide instead), the web can get closer to native back-navigation behavior. But it requires knowing about these pieces and assembling them yourself.

Fonts

On native platforms, bundled fonts are available instantly. There's no loading state, no layout shift, no fallback swap. On the web, custom fonts trigger either a flash of invisible text (FOIT) or a flash of unstyled text (FOUT) while the font file downloads. Getting native-like behavior means choosing a font-display strategy, adding <link rel="preload"> hints, and subsetting fonts to reduce file size. font-display: optional avoids the swap entirely by giving up on the custom font if it doesn't load in ~100ms, but that means some users see system fonts permanently. There is no option that gives you "font is ready on first paint" the way native does.

Startup

Native apps have bloated binaries too, but they pay that cost once at install. Code is compiled ahead of time, so launch is near-instant with no parsing step. On the web, the app ships as JavaScript that the browser must download, parse, and compile before anything is interactive. A 2MB bundle is common for SPAs with a component library, state management, and a few feature modules. That can take seconds to parse on a mid-range phone, and without a service worker the browser re-downloads it on every cold load.

Getting this under control means code splitting, lazy loading routes, tree-shaking unused code, and caching assets with a service worker. All of it works, but it's manual optimization that native doesn't need.

Offline and caching

Native apps are installed locally. They launch instantly, work offline, and cache data between sessions without any extra effort from the developer. On the web, achieving the same requires setting up a service worker with the right caching strategy: deciding what to precache, how to handle navigation requests, when to invalidate stale assets, how to manage updates without breaking the running app. Libraries like Workbox make this easier, but it's still opt-in complexity. Most web apps don't bother, so they show a blank screen on every cold load and break completely without a network connection.

The measurement gap doesn't help

Chrome has good performance tooling. DevTools, Lighthouse, the Performance Observer API, Long Animation Frames, Core Web Vitals. The tools exist. Most developers don't use them.

But even if they did, there's a cross-browser problem. CLS measurement is Chromium-only (the Layout Instability API never shipped in Firefox or Safari). Long Tasks and Long Animation Frames are Chromium-only. navigator.deviceMemory is Chromium-only. The Frame Timing API was proposed in 2014 and never implemented by any browser. The Memory Pressure API was archived in 2019 with zero implementations. [12]

Native platforms give you MetricKit on iOS (hang rate, launch time, memory, CPU, GPU, battery, crash diagnostics, all aggregated automatically) and Macrobenchmark on Android (startup time, scroll jank, frame timing). These are first-party, stable, production-grade measurement tools.

The web's performance APIs are a patchwork of Chromium experiments and abandoned proposals. The practical effect: if you want to monitor real-user performance across all browsers, there are gaps you cannot fill.

Frameworks add another layer

This post compares web platform to native platform, but most web developers never write raw DOM code. They write React, Vue, or Svelte, and the framework's abstraction adds its own defaults problem.

React re-renders entire component subtrees by default. Preventing unnecessary work means useMemo, useCallback, React.memo, and knowing when each one actually matters. The React Compiler is a step toward closing this gap, automating memoization decisions that developers previously had to make by hand. Vue and Svelte are more surgical with reactivity, but every framework has its own performance cliff that the docs don't warn you about until you hit it. The platform might have a fast path, but the framework sitting between you and the platform has its own opinions about how to get there.

Every success story followed the same playbook

Figma, Sheets, and Photoshop all bypassed the DOM, used WebAssembly for compute, rendered via WebGL or WebGPU, and pushed work off the main thread into Workers. That's expert-level architecture. VS Code took a different route: it stayed on the DOM, runs in Electron, and still outperforms many native IDEs. Both paths work, but neither is the default path. The web's performance ceiling is high. The floor is low. The distance between them is too large.

The web already won

Nobody wants to wrestle with platform-specific APIs that don't transfer. Swift and UIKit knowledge doesn't transfer to Android. Kotlin and Jetpack Compose knowledge doesn't transfer to iOS. But web skills work everywhere: browser, desktop via Electron, mobile via PWAs or Capacitor.

Desktop native was always "possible" in C++ or Qt or WPF. Electron won anyway. VS Code is an Electron app that outperforms many native IDEs. The "native is faster" argument lost to "web is portable and the talent pool is deeper."

Companies don't want three codebases. Developers don't want to learn APIs that become worthless when the platform shifts. Users don't want to install an app for everything. The web solves all three. That's why, even though native desktop apps in C++ were always possible, Electron is the clear winner for desktop software.

The web is eternal

A webpage from 2003 still loads. Try running an iOS 6 app or an Android 4.x app today.

Native platforms have forced migrations, deprecated APIs, removed apps from stores, and broken entire ecosystems with OS updates. The web's backwards compatibility guarantee is extraordinary. HTML, CSS, and JavaScript written twenty years ago still works in every modern browser. No other platform can claim this.

This durability matters more than raw performance for most software. A tool that works forever, everywhere, without an app store approval process or a mandatory SDK upgrade: that's a different category of reliable.

The gap is closing

The specific fixes for each of the defaults above are already shipping (View Transitions, content-visibility, moveBefore(), Navigation API, Scroll-Driven Animations). But the platform is also evolving at a deeper level.

CSS Anchor Positioning lets you position tooltips, popovers, and dropdowns relative to other elements declaratively, replacing entire JavaScript positioning libraries. [13] Customizable <select> with appearance: base-select gives full CSS control over native select elements, so you stop rebuilding dropdowns from scratch with divs. [14] CSS is getting if() for inline conditional logic, [15] @function for author-defined custom functions, [16] and scroll-state container queries that let you style elements based on whether a container is scrolled or stuck. [17]

Text measurement was another gap: native platforms like Core Text on iOS give cheap, programmatic access to text metrics and line layout, while the web required expensive DOM reflows to measure something as basic as text height. Pretext computes multiline text layout entirely outside the DOM, handling complex scripts, bidirectional text, and rendering to DOM, Canvas, or SVG. [18]

On the language side, TC39 is working on the fundamentals. Shared Structs (Stage 2) would let threads share objects directly with Atomics.Mutex for synchronization, replacing raw SharedArrayBuffer byte manipulation and postMessage serialization. [19] Signals (Stage 1) would put reactive primitives into the language itself, backed by the Angular, Vue, Solid, and Svelte teams. [20] import defer (Stage 3) enables lazy module evaluation for faster startup. [21]

The web isn't slow. It makes fast harder than it should be. That's changing.

References

  1. Figma: Rendering powered by WebGPU
  2. Addy Osmani: Photoshop is now on the web
  3. web.dev: Google Sheets WasmGC case study
  4. Jangda et al.: Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code (USENIX ATC 2019)
  5. web.dev: content-visibility Baseline
  6. Chrome Developers: moveBefore API
  7. Chrome Developers: View Transitions in 2025
  8. Chrome Developers: Updates in hardware-accelerated animation capabilities
  9. Motion.dev: Web animation performance tier list
  10. W3C: Scroll-driven Animations spec
  11. MDN: Navigation API
  12. WICG: Memory Pressure API (archived)
  13. Chrome Developers: CSS Anchor Positioning
  14. Chrome Developers: Customizable select element
  15. Chrome Developers: CSS conditionals with if()
  16. W3C: CSS Custom Functions and Mixins spec
  17. Chrome Developers: CSS scroll-state() queries
  18. chenglou: Pretext
  19. TC39: Structs proposal
  20. TC39: Signals proposal
  21. TC39: Deferred import evaluation