Arrêtez d'utiliser @capacitor/camera dans vos projets Ionic — voici pourquoi nous l'avons abandonné
Mobile
Performance
Ionic
Android
Capacitor

Arrêtez d'utiliser @capacitor/camera dans vos projets Ionic — voici pourquoi nous l'avons abandonné

Pourquoi nous avons remplacé @capacitor/camera après des ANR causés par des photos géantes et comment un plugin Kotlin maintient l'interface fluide.
Marwen.T
Marwen.T

Lead Software Engineer

October 26, 2025

8 min de lecture

Arrêtez d'utiliser @capacitor/camera dans vos projets Ionic — voici pourquoi nous l'avons abandonné

Lorsque nous avons construit notre application Ionic, @capacitor/camera semblait l'aide évidente : ouvrir l'appareil photo, choisir la galerie, récupérer une URI en une ligne de code. Pour la plupart des cas d'utilisation, le plugin fonctionne parfaitement bien—gérant les selfies typiques, les captures d'écran et les photos de galerie standard sans aucun problème.

Cependant, les choses ont commencé à se gâter lorsque nous sommes passés de la QA interne aux tests ouverts. Les utilisateurs ont commencé à sélectionner des images haute résolution, et c'est à ce moment-là que la Google Play Console s'est remplie d'ANR que nous pouvions attribuer directement au plugin. L'un de nos utilisateurs a fait planter son app en sélectionnant une image de plusieurs centaines de MB. Après enquête, nous avons découvert que les images d'environ 1200×1200 pixels et plus provoqueront le blocage du plugin @capacitor/camera et finiront par faire planter l'app sur les appareils milieu de gamme.

Comment tout a commencé

Sur nos propres appareils, rien ne plantait. Puis ce rapport est arrivé pendant l'open testing :

ANR: Input dispatching timed out
com.capacitorjs.plugins.camera.ImageUtils.transform

La stack trace vivait dans le plugin Capacitor Camera — loin de notre code. Un testeur anonyme a ensuite téléversé une photo énorme, donc nous avons reproduit le scénario avec un fichier surdimensionné.

La reproduction

Nous avons soumis à la sélection une image de 12 288 × 16 320 px (~200 MB). En quelques secondes, l'UI s'est figée, le tactile a cessé de répondre et le watchdog Android a tué l'application. Le logcat affichait :

E/Choreographer: Skipped 500+ frames!  The application may be doing too much work on its main thread.
E/OpenGLRenderer: Davey! duration=5000ms

Pourquoi un "simple" plugin caméra bloque-t-il ainsi l'interface ?

La cause racine : décodage sur le thread principal

Lorsque vous appelez l'API de confort :

const photo = await Camera.getPhoto({ quality: 90, resultType: CameraResultType.Uri, });

Le pipeline natif Capacitor continue de décoder et transformer le bitmap pleine résolution sur le thread principal. Même avec des options de largeur/hauteur, le plugin charge d'abord la taille originale puis redimensionne. Résultat : des centaines de mégaoctets qui bloquent la distribution des entrées avant même que votre logique ne démarre. Parfait pour un selfie, catastrophique pour des sources multi-mégapixels.

La solution : construire un plugin natif

Nous avons supprimé @capacitor/camera et expédié un petit plugin Kotlin qui :

  • Laisse l'utilisateur choisir entre galerie et appareil photo
  • Lit uniquement les dimensions avec inJustDecodeBounds=true sans décoder les pixels
  • Rééchantillonne les énormes sources sur un thread d'arrière-plan via inSampleSize
  • Compresse le résultat en JPEG dans le cache de l'application
  • Retourne { path, webPath, mimeType, width, height } au JavaScript

Plugin Kotlin

@CapacitorPlugin(name = "NativeImagePicker") class NativeImagePicker : Plugin() { @PluginMethod fun pickImage(call: PluginCall) { val maxW = call.getInt("maxWidth") ?: 4000 val maxH = call.getInt("maxHeight") ?: 4000 val intent = Intent(Intent.ACTION_PICK).apply { type = "image/*" } startActivityForResult(call, intent, "pickImageResult") } override fun handleOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode != Activity.RESULT_OK || data?.data == null) return val uri = data.data!! CoroutineScope(Dispatchers.IO).launch { val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true } context.contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, opts) } val sample = calculateInSampleSize(opts.outWidth, opts.outHeight, 6000, 6000) val decodeOpts = BitmapFactory.Options().apply { inSampleSize = sample } val bmp = context.contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, decodeOpts) } val outFile = File(context.cacheDir, "preview_${System.currentTimeMillis()}.jpg") outFile.outputStream().use { stream -> bmp?.compress(Bitmap.CompressFormat.JPEG, 90, stream) } val result = JSObject().apply { put("path", outFile.absolutePath) put("webPath", Capacitor.convertFileSrc(outFile.absolutePath)) put("mimeType", "image/jpeg") put("width", bmp?.width) put("height", bmp?.height) } call.resolve(result) } } private fun calculateInSampleSize(w: Int, h: Int, maxW: Int, maxH: Int): Int { var sample = 1 if (h > maxH || w > maxW) { val halfH = h / 2 val halfW = w / 2 while ((halfH / sample) >= maxH && (halfW / sample) >= maxW) { sample *= 2 } } return sample } }

Ce pipeline n'approche jamais le thread UI pendant le décodage, même si quelqu'un importe une image taille panneau publicitaire.

Fallback web pour les builds navigateur

Les plugins Capacitor ne s'exécutent pas dans le navigateur ; nous avons donc reproduit l'API avec un simple input fichier. C'est le secret qui rend notre app entièrement multiplateforme—la même base de code alimente notre app Android native et notre app web sur app.rembg.com, utilisant toutes deux la même API JavaScript.

import { WebPlugin } from '@capacitor/core'; export class NativeImagePickerWeb extends WebPlugin { async pickImage() { return new Promise((resolve) => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = () => { const file = input.files?.[0]; if (!file) return; resolve({ path: null, webPath: URL.createObjectURL(file), mimeType: file.type, }); }; input.click(); }); } }

Même API JS, deux implémentations. Une native (Kotlin) pour Android, une web (HTML input) pour les navigateurs—les deux gèrent la sélection d'images à la perfection sur toutes les plateformes.

Résultats après migration

  • ✅ Zéro ANR signalé dans Play Console depuis le switch
  • ✅ Plus de "Davey!" ni d'avertissements de frames sautées
  • ✅ Sélection instantanée, même pour 200 MB en 16K×12K
  • ✅ Empreinte mémoire et GC prévisibles
  • ✅ Parcours JS identiques pour les builds Android et web

Nous avons relancé le stress test 200 MB et l'UI est restée parfaitement fluide.

TL;DR

Si votre app Ionic ingère de grosses images, @capacitor/camera surcharge le thread principal.

  • Lisez d'abord les dimensions, réduisez en arrière-plan et compressez hors du thread UI
  • Retournez seulement des métadonnées légères au JavaScript
  • Gardez un fallback navigateur pour conserver l'API cohérente

Le bridge Capacitor vous donne la main sur le pipeline natif. Nous avons abandonné le plugin caméra officiel et nos ANR ont disparu du jour au lendemain.


Ready to Try RemBG's API?

Start removing backgrounds with our powerful API. Get 60 free credits to test it out.

Get API AccessTry Free Tool