ClaudeExpoJun 10, 2026, 1:15 PM

How a Kotlin compiler plugin cut Android time to first render by 30%

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

claudeen

How a Kotlin compiler plugin cut Android time to first render by 30% Summary

Key Points

  • Point 1: Expo SDK 56 ships a Kotlin compiler plugin that removes reflection from Expo Modules on Android.
  • Point 2: The numbers: 70% faster module initialization, a 30% cut in time to first render, and Record conversions that run about 6x faster than in SDK 55.
  • Point 3: If you're building an app, you get those gains automatically - the plugin runs during compilation, with no code changes on your side.

Summary

This is an English summary of "How a Kotlin compiler plugin cut Android time to first render by 30%" published on 2026-06-10.

Key Points

  • Point 1: Expo SDK 56 ships a Kotlin compiler plugin that removes reflection from Expo Modules on Android.
  • Point 2: The numbers: 70% faster module initialization, a 30% cut in time to first render, and Record conversions that run about 6x faster than in SDK 55.
  • Point 3: If you're building an app, you get those gains automatically - the plugin runs during compilation, with no code changes on your side.

Full Translation

Translations

A translation section that keeps the flow of the original article.

claudeja

How a Kotlin compiler plugin cut Android time to first render by 30%(原文タイトル)

概要

公開日: 2026-06-10 翻訳生成に失敗したため、原文をそのまま保存しています。

原文

Expo SDK 56 ships a Kotlin compiler plugin that removes reflection from Expo Modules on Android. The numbers: 70% faster module initialization, a 30% cut in time to first render, and Record conversions that run about 6x faster than in SDK 55. If you're building an app, you get those gains automatically - the plugin runs during compilation, with no code changes on your side. If you maintain a module, the Record speedup is one annotation away. This post covers how we got here, and why this particular approach worked when others didn't. For the Apple side, where Swift now talks to JSI directly, see the companion post Talking to JSI in Swift. Reflection and the history behind it Before Expo Modules existed, we had Unimodules. They worked a lot like the old React Native bridge modules: you'd scatter annotations across methods that you wanted to expose, and the runtime would discover everything through reflection. class ClipboardModule(context: Context) : ExportedModule(context) { override fun getName() = "ExpoClipboard" @ExpoMethod fun getStringAsync(promise: Promise) { val clip = clipboardManager.primaryClip?.getItemAt(0) promise.resolve(clip?.text?.toString() ?: "") } @ExpoMethod fun setStringAsync(content: String, promise: Promise) { clipboardManager.setPrimaryClip(ClipData.newPlainText(null, content)) promise.resolve(true) } } If you need metadata about your own code, reflection is the obvious tool. What methods does a module export? What arguments do they take? Just ask the JVM. But reflection has a cost, and on Android that cost lands directly on startup time. Every module the runtime introspects adds milliseconds before the user sees anything. When we started building the Expo Modules API we have today, we wanted two things: better ergonomics and less reflection. The Kotlin DSL was the easy win because it gave us the ergonomics and removed most of the reflection in one move. What it couldn't remove was the type information for function arguments and Record properties. Resolving those still meant runtime reflection - concretely, a typeOf<T>() call and the metadata lookups behind it - and that was the cost we couldn't fix with the DSL alone. The real cost of reflection That remaining cost comes in two parts. The first is reconstructing type parameters. The DSL reads argument and return types through typeOf<T>(), which works because T is reified. Normally the JVM erases generics, so at runtime you can't ask what T actually was - the information is gone by the time the code runs. Reified type parameters get around that, letting us read the concrete type. It works because typeOf lives in an inline function: the compiler copies it into each call site and substitutes the real type in directly. Retrieving type information this way is cheap in most cases, but it adds up when a module has many functions or deeply nested generics. The second, and the heavier one, is Record conversion. A Record is our typed representation of a JS object on the native side. To convert one, we have to discover its shape at runtime: which properties it declares, which are exposed to JS, and what type each one has. The cost of that discovery is high because it involves multiple layers of reflection. You have to ask the JVM for the class's memberProperties, then ask each property for its annotations and type, then make the field accessible to write to it. Also, not all of that information is directly available in bytecode. The JVM knows about classes and their members, but it doesn't know about Kotlin's type system. The Kotlin reflection library has to reconstruct that information by parsing the @Metadata annotation, which is a binary blob that the compiler generates. Some of this we could sidestep. Top-level nullability, for example, doesn't need full reflection - with a reified T, a simple null is T check answers it. The nested cases (like the T in List<T>) are a different story. The JVM erases generics, so the type arguments are gone from the bytecode at