Guides10 min read

Make a Capacitor app feel native: a performance checklist

Twenty concrete fixes for slow or web-feeling Capacitor apps: startup, splash screen, bundle size, scrolling, touch, keyboard, images and memory. How to measure each one on a real device.

“It feels like a website” is the most common complaint about Capacitor apps, and it is almost never Capacitor's fault. The WebView on a current iPhone or Android phone is fast. What makes an app feel slow is a large JavaScript bundle on startup, a splash screen that hides too early, scrolling that bounces the whole page, and taps that highlight like links.

This checklist covers the fixes that make the biggest difference, grouped by where users notice them. Nearly all of them are web changes.

First, measure on a real device

Your laptop hides most performance problems. Test on a mid-range Android phone, which is what many of your users have.

  • iOS: enable Web Inspector on the device, then use Safari's Develop menu to open the Timelines tab for your app.
  • Android: open chrome://inspect in desktop Chrome with the phone connected and use the Performance panel.
  • Always test a production build, never the dev server.

Startup

  1. Hide the splash screen yourself. Set launchAutoHide: false and call SplashScreen.hide() after your first screen has rendered. Otherwise users see a blank white WebView between the splash and your UI.
// capacitor.config.ts
plugins: {
  SplashScreen: { launchAutoHide: false },
},

// after the first screen renders
import { SplashScreen } from '@capacitor/splash-screen';
await SplashScreen.hide();
  1. Ship less JavaScript at startup. Split by route and lazy-load screens the user does not see first. Settings, onboarding and admin screens rarely belong in the main chunk.
  2. Do not wait for the network to show something. Render from cached data and refresh in the background. An app that shows yesterday's data instantly feels faster than one that shows a spinner.
  3. Move non-urgent work after first paint. Analytics, feature flag fetches, remote config and error reporting setup can wait until the UI is visible.
  4. Bundle your fonts. Load fonts from your app's assets, not a font CDN, and use font-display: swap. See reducing bundle size.

Touch and scrolling

These CSS rules remove most of the “website” feel in one go:

html, body {
  overscroll-behavior: none;          /* no rubber-banding of the whole page */
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;        /* no long-press link preview */
}

body {
  user-select: none;                  /* allow it again on real text fields and content */
  -webkit-user-select: none;
}

input, textarea, [contenteditable], .selectable {
  user-select: text;
  -webkit-user-select: text;
}

button, a, [role="button"] {
  touch-action: manipulation;         /* no double-tap zoom delay */
}
  1. Scroll inside containers, not the body. Fix the header and tab bar and let the content area scroll. The app then behaves like a native screen instead of a page.
  2. Give feedback on press. A subtle :active style, or a light haptic via @capacitor/haptics for important actions, makes taps feel registered.
  3. Virtualize long lists. Rendering 2,000 rows kills scrolling on mid-range Android. Render only what is visible.

Animation and rendering

  1. Animate only transform and opacity. Animating width, height, top or left forces layout on every frame.
  2. Avoid heavy blur and big shadows on scrolling content. backdrop-filter on a list is expensive, especially on Android.
  3. Keep the main thread free. Move parsing, sorting and crypto for large data into a Web Worker.
  4. Respect reduced motion. Use @media (prefers-reduced-motion: reduce) to shorten or remove transitions.

Images and memory

  1. Serve images at display size. A 4000px photo in a 120px thumbnail wastes memory and decode time. Resize on the server or at upload.
  2. Use modern formats. WebP and AVIF are supported by current WebViews and are much smaller than JPEG and PNG.
  3. Lazy-load offscreen images with loading="lazy".
  4. Clean up. Remove event listeners, stop intervals and release object URLs when screens unmount. Leaks add up in an app that stays open for days. Google Play is adding memory usage thresholds to its quality requirements in February 2027, so this matters for Android visibility too.

Native details

  1. Configure the keyboard. With @capacitor/keyboard, choose the resize mode that fits your layout, so inputs are not hidden and the page does not jump.
  2. Match the status bar and safe areas. A status bar that does not match your header is the fastest giveaway. See edge-to-edge display.
  3. Use native plugins where the web is weak. Haptics, share sheets, file pickers and biometrics feel wrong when imitated in HTML.
  4. Make it work offline. Cache the data users need most and show it without a network. See offline support.

Most of these fixes are web changes. Only new native plugins and changes to plugin settings in capacitor.config.ts (which is copied into the native project at sync time) need a store build. Everything else can ship as a web update.

Ship performance work continuously

Performance work goes best in small steps: change one thing, measure on a real device, ship, check real-user numbers, repeat. That loop breaks down when every step waits for App Review.

With OtaKit, each improvement goes to users as a web update, and delta updates mean devices download only the files that changed. Release to a small group first with a staged rollout, compare, and then release to everyone. Small, frequent improvements are how an app that feels like a website becomes one that feels native.

Related docs