ImageVector vs XML drawable, a performance guide

Farbod Bijary
4 min readMar 19, 2024

The first Time I used Jetpack Compose in-built icons using Icons.Default.<ICON_NAME> , I wondered how they work under the hood, from working with Views for some time I knew that the flow for drawing a vector drawable resource was as follows:

  1. Parsing the XML
  2. Vector Drawable Creation
  3. View Measurement and Layout
  4. Canvas Drawing
  5. Path Processing and Rendering
  6. Painting

I took a look into the source code of the Icons class and found out those ImageVectors where stored as created using a PathBuilder which stored paths needed for drawing the vector asset as constants in the code, for example theIcons.Default.Delete in androidx.compose.material.icons looks like this:

materialPath {
moveTo(6.0f, 19.0f)
curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f)
horizontalLineToRelative(8.0f)
curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
verticalLineTo(7.0f)
horizontalLineTo(6.0f)
verticalLineToRelative(12.0f)
close()
moveTo(19.0f, 4.0f)
horizontalLineToRelative(-3.5f)
lineToRelative(-1.0f, -1.0f)
horizontalLineToRelative(-5.0f)
lineToRelative(-1.0f, 1.0f)
horizontalLineTo(5.0f)
verticalLineToRelative(2.0f)
horizontalLineToRelative(14.0f)
verticalLineTo(4.0f)
close()
}

It seemed pretty obvious to me that parsing and showing a vector drawable that is stored in the code would cost less since it does not need to read and parse the XML (step 1) and also creating the ImageVector would take less time due to the decreased overhead as a result of the drawable being stored in a Kotlin file which will later be compiled into the binaries. But experience has shown finding out and about performance differences should always be done using an appropriate benchmark run repeated times.

Let’s Benchmark

1. Load and Render time

For getting better metrics on performance of these methods I took some SVGs from Telegram’s source code available at Telegram Github repo which were pretty large (larger than 200x200) and complex. Then I used Svg to Compose IDE plugin to create an Icon pack containing 8 ImageVectors from my vector assets. After importing all my drawables in two forms (8 ImageVectors and 8 XML drawables) I created two Composables:

@Preview
@Composable
fun SvgPerfTest() {
Column {
Row {
Icon(imageVector = MyIconPack2.PermissionPinDark, contentDescription = "", tint = Color.Unspecified)
Icon(imageVector = MyIconPack2.PipVideoRequest, contentDescription = "", tint = Color.Unspecified)
Icon(imageVector = MyIconPack2.PipVoiceRequest, contentDescription = "", tint = Color.Unspecified)
}
Row {
Icon(imageVector = MyIconPack2.QrDog, contentDescription = "", tint = Color.Unspecified)
Icon(imageVector = MyIconPack2.QrLogo, contentDescription = "", tint = Color.Unspecified)
Icon(imageVector = MyIconPack2.RecordAudio, contentDescription = "", tint = Color.Unspecified)
}
Row {
Icon(imageVector = MyIconPack2.RecordVideoL, contentDescription = "", tint = Color.Unspecified)
Icon(imageVector = MyIconPack2.RecordVideoP, contentDescription = "", tint = Color.Unspecified)
}
}
}

@Preview
@Composable
fun XmlPerfTest() {
Column {
Row {
Icon(painter = painterResource(id = R.drawable.permission_pin_dark), contentDescription = "", tint = Color.Unspecified)
Icon(painter = painterResource(id = R.drawable.pip_video_request), contentDescription = "", tint = Color.Unspecified)
Icon(painter = painterResource(id = R.drawable.pip_voice_request), contentDescription = "", tint = Color.Unspecified)
}
Row {
Icon(painter = painterResource(id = R.drawable.qr_dog), contentDescription = "", tint = Color.Unspecified)
Icon(painter = painterResource(id = R.drawable.qr_logo), contentDescription = "", tint = Color.Unspecified)
Icon(painter = painterResource(id = R.drawable.record_audio), contentDescription = "", tint = Color.Unspecified)
}
Row {
Icon(painter = painterResource(id = R.drawable.record_video_l), contentDescription = "", tint = Color.Unspecified)
Icon(painter = painterResource(id = R.drawable.record_video_p), contentDescription = "", tint = Color.Unspecified)
}
}
}

Then I created a simple benchmark using Macrobenchmark library to repeatedly measure simple metrics like time to initial display, frame duration and frame overrun for each method. Each test case was repeated 40 times with cold starting the app each time:

@RunWith(AndroidJUnit4::class)
class VectorAssetBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()

@Test
fun SVG_Test() = benchmarkRule.measureRepeated(
packageName = "com.me.testapp",
metrics = listOf(
StartupTimingMetric(),
FrameTimingMetric(),
),
iterations = 40,
startupMode = StartupMode.COLD,
) {
startActivityAndWait { intent ->
intent.putExtra("sut", "svg")
}
pressHome()
}

@Test
fun XML_Test() = benchmarkRule.measureRepeated(
packageName = "com.me.testapp",
metrics = listOf(
StartupTimingMetric(),
FrameTimingMetric(),
),
iterations = 40,
startupMode = StartupMode.COLD,
) {
startActivityAndWait { intent ->
intent.putExtra("sut", "xml_1")
}
pressHome()
}
}

And after running the benchmark these were the results I got:

VectorAssetBenchmark_SVG_Test
timeToInitialDisplayMs min 346.6, median 373.8, max 823.1
frameDurationCpuMs P50 122.1, P90 145.8, P95 150.7, P99 257.8
frameOverrunMs P50 107.1, P90 134.4, P95 140.2, P99 241.8

VectorAssetBenchmark_XML_Test
timeToInitialDisplayMs min 350.9, median 387.7, max 894.3
frameDurationCpuMs P50 139.8, P90 154.0, P95 169.5, P99 279.6
frameOverrunMs P50 125.6, P90 143.8, P95 153.7, P99 266.4

2. Memory and CPU usage

Another obvious factor needed for this benchmark was seeing how much of the heap and cpu time rendering these drawables take. This was done by profiling with complete data, this method puts some overhead on the performance of the app but gives pretty accurate data.

for both composables the heap and cpu usage was nearly identical and I could not see any difference. The memory taken by the app was rarely above 64MB and CPU usage almost never exceeded 20%.

profiler result created by android studio

Conclusion

In a single screen of an android app it’s unlikely to have a large number of large and complex vector drawabels and if such situation occurs you may want to think of alternative ways of doing what you want instead of vector assets. But all in all both methods were able to load and render vector drawables pretty fast and the SVGs which were converted to ImageVector being a little faster in terms of render time. But this amount is unlikely to make significant difference in most use cases. So if your app yanks when loading pages or is too slow don’t blame it on the SVGs there are probably other bottlenecks you are missing :)

--

--