Performance¶
Shared WebView via HighlightThemeProvider¶
The most impactful optimization is wrapping your content in HighlightThemeProvider. Without it, every SyntaxHighlightedCode creates its own hidden WebView:
| Setup | WebViews | Avg per block | Total (17 blocks) | Heap (cold start) |
|---|---|---|---|---|
| No provider - 17 blocks | 17 | ~50 ms | ~866 ms | ~34 MB |
| With provider - 17 blocks | 1 | ~13 ms | ~224 ms | ~15 MB |
Measured on a Pixel 8 Pro, debug build, cold start. With provider is roughly 4x faster and uses ~55% less heap.
Each extra standalone engine adds roughly ~37 ms to average highlight time and ~1-2 MB of additional heap per engine while the screen is active.
import dev.hossain.highlight.ui.HighlightThemeProvider
import dev.hossain.highlight.ui.SyntaxHighlightedCode
import dev.hossain.highlight.ui.rememberTomorrowNightTheme
import dev.hossain.highlight.ui.rememberTomorrowTheme
// Wrap once, high up in your composition tree
HighlightThemeProvider(
lightHighlightTheme = rememberTomorrowTheme(),
darkHighlightTheme = rememberTomorrowNightTheme(),
) {
LazyColumn {
items(snippets) { snippet ->
SyntaxHighlightedCode(code = snippet.code, language = snippet.lang)
}
}
}
Pre-warming the WebView¶
The WebView initializes lazily on the first highlight() call. To hide that cost, warm it up during app start:
// Application.onCreate() - pre-warm the Android WebView renderer process
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
runCatching {
WebViewCompat.startUpWebView(
applicationContext,
WebViewStartUpConfig.Builder(mainExecutor).build(),
WebViewOutcomeReceiver { /* no-op */ },
)
}
}
}
Requires androidx.webkit:webkit:1.16+ (a transitive dependency of this library).
Pre-warming via HighlightEngine¶
When using HighlightEngine directly in a ViewModel, call initialize() on a background coroutine before you need the first highlight:
import dev.hossain.highlight.engine.HighlightEngine
import dev.hossain.highlight.engine.HighlightTheme
class CodeViewModel(application: Application) : AndroidViewModel(application) {
private val engine = HighlightEngine(application.applicationContext)
init {
viewModelScope.launch {
engine.initialize() // warm-up on launch
}
}
suspend fun highlight(code: String, language: String, theme: HighlightTheme) =
engine.highlight(code, language, theme).getOrNull()?.annotated
override fun onCleared() { engine.destroy() }
}
Highlight both themes for instant switching¶
Tokenization is the slow part. Running it twice for light + dark wastes time. Use highlightBothThemes to tokenize once and produce both variants:
import dev.hossain.highlight.ui.rememberTomorrowNightTheme
import dev.hossain.highlight.ui.rememberTomorrowTheme
val result = engine.highlightBothThemes(
code = sourceCode,
language = "kotlin",
lightTheme = rememberTomorrowTheme(),
darkTheme = rememberTomorrowNightTheme(),
)
// Switch instantly at the call site - no re-highlighting needed
result.onSuccess { themed ->
val annotated = if (isDark) themed.dark else themed.light
}
Inside Compose, use rememberHighlightedCodeBothThemes(code, language).
Timing callbacks¶
Monitor per-stage latency with onHighlightComplete:
import dev.hossain.highlight.ui.SyntaxHighlightedCode
SyntaxHighlightedCode(
code = snippet,
language = "kotlin",
onHighlightComplete = { result ->
Log.d(
"HighlightPerf",
"total=${result.durationMs}ms, " +
"js=${result.timings.jsBridge.inWholeMilliseconds}ms, " +
"parse=${result.timings.htmlParse.inWholeMilliseconds}ms, " +
"spans=${result.spanCount}",
)
},
)
HighlightTimings has individual fields for each pipeline stage:
| Field | Type | Description |
|---|---|---|
jsBridge |
Duration |
WebView JS evaluation |
jsonUnescape |
Duration |
JS string unescaping |
htmlParse |
Duration |
jsoup HTML parsing |
treeWalk |
Duration |
Span tree walk |
themeParse |
Duration |
CSS to SpanStyle (first call only; Duration.ZERO on cache hits) |
total |
Duration |
Full elapsed wall-clock time |
Use .inWholeMilliseconds to get a Long, or .inWholeNanoseconds for finer granularity.
Typical latencies (Pixel 6, release build)¶
| Stage | First call | Subsequent calls |
|---|---|---|
| WebView warm-up | ~150-200 ms | 0 ms |
| JS evaluation | ~20-50 ms | ~5-15 ms |
| HTML parse + span walk | ~2-5 ms | ~2-5 ms |
| Theme CSS parse | ~10-20 ms (once) | 0 ms (cached) |
isInitialized state flow¶
Observe engine readiness reactively - for example to show a loading indicator:
import dev.hossain.highlight.ui.SyntaxHighlightedCode
import dev.hossain.highlight.ui.rememberHighlightEngine
val engine = rememberHighlightEngine()
val isReady by engine.isInitialized.collectAsState()
if (!isReady) {
CircularProgressIndicator()
} else {
SyntaxHighlightedCode(code = snippet, language = "kotlin")
}