[2026] Capacitor complete guide — from web to native hybrid apps
이 글의 핵심
Capacitor loads your built web assets into a native shell and connects JavaScript to OS APIs through a thin, predictable bridge. This article covers architecture, iOS/Android project structure, official plugins (Camera, Filesystem, etc.), custom native plugins, push and background limits, performance tuning, and how to choose versus Cordova.
What this article covers
Capacitor is a cross-platform runtime from the Ionic team. It lets you ship any web frontend build (Vue, React, Angular, Svelte, etc.) to iOS, Android, and the web (PWA). In hybrid apps, what matters is not only that the UI is drawn with web tech, but that you keep the bridge safe inside native lifecycle, permissions, and store policies.
This guide walks through (1) architecture and core concepts, (2) iOS/Android native project layout, (3) official plugin APIs, (4) custom plugin development, (5) realistic limits for push and background, (6) a performance checklist, and (7) Cordova comparison.
1. Core Capacitor concepts
1.1 Native shell and web assets
A Capacitor app is not merely “a browser opening a URL.” Static web assets (webDir) are copied into the native project produced by Xcode/Android Studio, and WKWebView (iOS) or Android System WebView loads them. During development you can point at a dev server with server.url for an experience close to hot reload.
1.2 Capacitor runtime and bridge
On the JavaScript side, @capacitor/core handles the plugin registry and message passing. On the native side, the Capacitor runtime invokes implementations that match the same plugin and method names. Because that contract is explicit, TypeScript definitions line up well, and new plugins follow a repeatable pattern.
1.3 What npx cap sync does
sync (1) copies your web build to the right place (e.g. android/app/src/main/assets/public) and (2) aligns native dependencies and plugin hooks with Gradle / CocoaPods. This is why rebuilding “only the web” does not show up in a store build until you sync. For release builds, always follow web build → cap sync → native build.
1.4 Central config: capacitor.config
One place holds app ID (appId), display name (appName), web asset path (webDir), dev server URL, and iOS/Android-specific options. Larger teams often split config by environment (staging API, logging) in capacitor.config.ts, or swap files in the build pipeline.
// capacitor.config.ts (example structure)
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'MyApp',
webDir: 'dist',
server: {
// Dev only — remove or disable via build script for production
// url: 'http://192.168.0.10:5173',
cleartext: true,
},
ios: {
contentInset: 'automatic',
},
android: {
allowMixedContent: false,
},
};
export default config;
webDir must match your production build output from Vite, Webpack, Angular CLI, etc. If you ship a store archive with server.url enabled, the app can depend on external networking and fail review—use a separate production config in CI.
1.5 Understand the security model
Hybrid apps deal with custom URL schemes (capacitor://, etc.), mixed content, and HTTP in debug builds. For production, prefer HTTPS only, disable unnecessary cleartext, and add only trusted origins to allowNavigation. Many teams avoid leaving sensitive tokens in WebView storage alone and use Keychain / Keystore wrapper plugins instead.
2. iOS and Android project layout
2.1 Typical setup flow
- Get your web project to a state where a production build works.
npm install @capacitor/core @capacitor/cli, thennpx cap initwith app ID andwebDir.npx cap add iosandnpx cap add androidto create native folders.npx cap syncto copy assets and align native dependencies.npx cap open iosornpx cap open androidto open the IDE.
If Xcode, CocoaPods, Android SDK, and JDK versions differ per developer, you get “works on my machine” issues. Maintain a pinned version doc (e.g. Xcode 16.x, Android Gradle Plugin x.x).
2.2 iOS: what to inspect in Xcode
- Bundle ID & Signing & Capabilities: enable push, background modes, app groups, etc.
- Info.plist: purpose strings for camera, mic, photo library (
NSCameraUsageDescription, etc.) are required for App Store review. - AppDelegate / Scene lifecycle: ties into deep links, push token refresh, and background entry.
- WKWebView: behavior varies by iOS version—define minimum OS and a test matrix.
2.3 Android: Gradle and manifest
- Keep applicationId consistent with
appIdincapacitor.config. - minSdkVersion / targetSdkVersion: Play policies move forward—raise them on a schedule.
- AndroidManifest.xml: permissions,
android:usesCleartextTraffic, background services/workers. - Proguard/R8: ensure release obfuscation does not strip plugin classes.
2.4 Mental model of the folder tree
The repo root often has src/ (frontend), dist/ (build), ios/, and android/. If you edit native code by hand, Capacitor upgrades can conflict—isolate custom native work in plugins or keep documented patch files.
flowchart LR
subgraph web [Web build]
A[Source TS/Vue/React] --> B[Bundler build]
B --> C[webDir output]
end
subgraph cap [Capacitor]
C --> D[npx cap sync]
D --> E[Copy to iOS/Android]
end
subgraph native [Native]
E --> F[Xcode / Android Studio build]
F --> G[IPA / AAB·APK]
end
3. Native plugin APIs
Official @capacitor/* packages wrap native features in Promise-based APIs. The usual pattern is import { PluginName } from '@capacitor/...' then PluginName.method(). Always design permission denial, cancel, and timeout together with your UI.
3.1 Camera
@capacitor/camera provides photo capture and the gallery picker. Consider permission copy, privacy, large image memory, and EXIF metadata. Whether to resize/compress before upload on the web side or natively depends on your team.
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
export async function pickPhoto() {
const image = await Camera.getPhoto({
quality: 85,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Prompt, // Camera vs gallery chooser UI
});
// image.webPath — path usable inside the WebView
return image;
}
In production, prefer file URI streams over Base64 for uploads when memory matters. On iOS, account for Photos permission scope (limited library) when designing UX.
3.2 Filesystem
@capacitor/filesystem abstracts DATA, CACHE, DOCUMENTS, and similar storage areas. Use it for offline caches, logs, and user-generated content. Prefer API-provided URIs and directory constants over hand-built path strings to absorb OS differences.
For large files, design chunked read/write and progress UI, and verify compatibility with Android scoped storage.
3.3 Other common official plugins
| Area | Representative plugins | Notes |
|---|---|---|
| Device info | App, Device | Model and OS version for analytics and compatibility branches |
| Network | Network | Offline banners, retry triggers for sync queues |
| Storage | Preferences | Small KV; review extra encryption for sensitive data |
| UI | Toast, Dialog, Status Bar, Splash Screen | Native look and feel |
| Hardware | Haptics, Geolocation | Mind permissions and battery |
Each plugin has its own minimum OS and permission declarations—validate on a real-device matrix before submission.
4. Custom plugin development
4.1 When to build custom
Use custom plugins when in-house SDKs, special peripherals, or security policy require code that must live only in native code. Capacitor lets you implement methods in Swift/Objective-C (iOS) and Kotlin/Java (Android) and call them from TypeScript with the same identifiers.
4.2 Design checklist
- Fix plugin and method names to team conventions.
- Keep inputs and outputs JSON-serializable where possible so bridge cost stays predictable.
- Main-thread rules: UI on main thread; heavy work on background queues, then bridge results back.
- Map error codes to a shared enum for branching in JS.
4.3 TypeScript stub example
import { registerPlugin } from '@capacitor/core';
export interface EchoPlugin {
echo(options: { value: string }): Promise<{ value: string }>;
}
export const Echo = registerPlugin<EchoPlugin>('Echo', {
web: () => import('./echo-web').then((m) => new m.EchoWeb()),
});
If you target web (PWA) too, provide a web implementation so you can develop against one interface. For mobile-only apps, return a clear no-op or error from web.
4.4 Native side
On iOS, expose @objc methods on a CAPPlugin subclass; on Android, use @PluginMethod on a Plugin class. Write platform code per Swift Concurrency / Kotlin coroutine guides, but honor Capacitor’s callback and error contract.
If native experience is thin, prefer one cohesive plugin over many tiny ones to control maintenance cost.
5. Push notifications and background work
5.1 Push: FCM / APNs
@capacitor/push-notifications helps with local notification scheduling and remote push token registration. You still design the server infrastructure that actually sends pushes (FCM HTTP v1, APNs keys/certificates) and token APIs with your backend.
In production, revisit:
- Token refresh and server sync
- Foreground receive behavior and in-app display policy
- Deep link payload and routing
- Android channels and iOS categories
5.2 Background reality
Mobile OSes strictly limit background execution for battery and privacy. Do not expect Capacitor alone to run always-on workers like on desktop.
- iOS:
Background Tasks,BGAppRefresh, location updates, push wake—each use case has a prescribed frame. - Android: WorkManager, foreground services (notification required)—policies change; follow official docs.
Treat experimental APIs like @capacitor/background-runner as documented and limited. If periodic sync is core to the business, plan native design early.
5.3 Design recommendations
- Prefer push-triggered + short work when possible
- Process offline queues when the app resumes
- For location tracking, comply with foreground service and battery disclosure policies
6. Performance optimization
6.1 Suspect the WebView first
If profiling shows JS execution, layout, and paint, prioritize frontend tuning over native tweaks. Long lists: virtual scrolling; images: lazy load and sensible resolution; main-thread work: chunking and Web Workers.
6.2 Startup
- Reduce splash and initial bundle size
- Route-level code splitting
- Load only essential APIs up front; lazy-
importthe rest
6.3 Native side
- Plan state recovery around WKWebView process restarts
- Use hardware acceleration and debug WebView for tracing
- On Android, reduce excessive overdraw and shadows
6.4 Memory
Camera, files, and maps can cause large memory spikes. Prefer streaming uploads, revoking object URLs, and downsampling images.
7. Cordova vs Capacitor
7.1 Philosophy and structure
Cordova has a long history and a huge plugin ecosystem. Capacitor treats native projects as first-class source and aligns with npm-centric tooling. Capacitor can use some Cordova plugins via a compatibility layer, but not every plugin works.
7.2 Decision guide
| Criterion | Lean Cordova | Lean Capacitor |
|---|---|---|
| Existing assets | Heavy Cordova plugin reliance | Modern web stack and bundlers |
| Native customization | Minimal maintenance | Frequent Xcode/Gradle edits |
| Team skills | Legacy-heavy | Strong TypeScript and modern front end |
| Long-term roadmap | Maintenance mode | New features and store policy updates |
New projects usually favor Capacitor; legacy projects often need a costed, gradual migration.
8. CLI commands and daily workflow
Handy command summary for onboarding docs—verify flags with npx cap --help and your installed @capacitor/cli version.
| Command | Purpose |
|---|---|
npx cap init | Initialize Capacitor metadata (app ID, name, webDir) |
npx cap add ios / android | Create native folders (avoid overwriting if they already exist) |
npx cap sync | Copy web build + sync native dependencies |
npx cap copy | Copy assets only (subset of sync; use sync if Gradle/Pods must update) |
npx cap open ios / android | Open Xcode / Android Studio |
npx cap run ios / android | Build and run from CLI (requires local tooling) |
Daily loop: (1) develop in web → (2) npm run build → (3) npx cap sync → (4) run from the IDE. On release branches, have CI verify server.url is off and the correct env vars are injected.
9. Troubleshooting: common blockers
9.1 White screen / missing index.html
Often webDir does not match the build output, or a wrong base path 404s assets. Use the browser devtools Network tab to confirm first HTML/JS loads; with Vite, check base matches Capacitor’s root loading path.
9.2 "Plugin ... is not implemented on android"
Common when JS calls a plugin but package registration or Gradle deps are missing. After npm install @capacitor/..., run npx cap sync again; on Android, confirm plugins load in MainActivity (templates differ by version).
9.3 API fails only on iOS
Often missing usage strings, Capabilities, or simulator vs device differences. Camera, mic, and location especially need real devices and Info.plist review.
9.4 CORS and mixed content
The WebView faces the same CORS rules as a browser for remote APIs. Mitigations: (1) fix API CORS headers, (2) an app-specific gateway, or (3) a native proxy custom plugin. Do not leave HTTP open in production builds.
10. Deep links and universal links (concept)
To open app screens instead of the web for specific URLs, configure iOS Universal Links and Android App Links. Capacitor does not replace all of this—you still need domain verification (AASA, Digital Asset Links) and native manifest / Associated Domains setup. After implementation, test on real devices: tap link → app opens, back navigation, etc.
11. Operations checklist
- Environment separation: dev/stage/prod APIs, logging, block
server.urlin prod - Version policy: minimum OS, WebView version, Play/App Store review notes
- Security: mixed content, certificate pinning if needed, encrypted storage
- Observability: crash reporting (e.g. Sentry), unified native + JS stacks
- CI: automate web build →
cap sync→ native build artifacts
12. Summary
Capacitor is a practical bridge between web delivery speed and store distribution. Successful hybrid apps are judged less on “we used web tech” and more on whether lifecycle, permissions, performance, and security were handled like a native app. If you follow this flow—layout → official plugins → custom when needed → push/background reality → performance and Cordova comparison—you avoid costly mistakes early in design.
Before deploy, follow your workflow: git add, git commit, git push, then npm run deploy.