성능
캡처 비용과 빌드 시간 측정 결과
요약
비용은 브릿지가 값을 받는 형태에 따라 달라집니다.
// 파라미터를 나눠 받는 형태: 값마다 파라미터가 따로 있음
@JavascriptInterface
fun onBridgeRequest(handlerName: String, requestId: String, data: String?)
// 매핑: handlerName = arg(0); requestId = arg(1); data = arg(2)
// JSON 문자열 하나로 받는 형태: 모든 값이 문자열 하나 안에 있음
@JavascriptInterface
fun postMessage(json: String) // {"handlerName":"…","requestId":"…","data":{…}}
// 매핑: handlerName = json(arg(0), "handlerName"); requestId = json(arg(0), "requestId") …기준은 파라미터 개수가 아니라, 값을 꺼내려고 플러그인이 JSON을 파싱해야 하는지입니다. 파라미터가 하나여도 그 값이 데이터 자체이고 json() 없이 매핑하면, 파라미터를 나눠 받는 형태와 비용이 같습니다.
- 파라미터를 나눠 받는 형태(
arg(n)): 모든 크기에서 플러그인과 직접 호출의 비용이 같습니다. 10 KB까지 왕복당 약 0.13–0.24 ms, 1 MB에서 약 2.7 ms입니다. 비용은 훅이 아니라 항목 기록에 듭니다. - JSON 문자열 하나로 받는 형태(
json()): 100 B에서는 비슷하고, 10 KB부터는 직접 호출이 더 빠릅니다. 10 KB에서 3.0 ms 대 4.8 ms, 1 MB에서 364 ms 대 697 ms입니다. 훅마다 메시지를 한 번 더 파싱하고data를 다시 문자열로 만들지만, 직접 호출은 브릿지가 이미 파싱한 객체를 재사용하기 때문입니다. - 릴리스 빌드는 변환되지 않으므로 비용이 없습니다.
dari-noop을 쓰는 debug 빌드는 호출당 몇 µs입니다. - 빌드 시간:
PROJECTscope는 측정되는 추가 시간이 없고,ALL은 clean 빌드가 확실히 느려집니다.
어떤 방식을 쓸까. 파라미터를 나눠 받거나 메시지가 작은 브릿지는 플러그인으로 충분합니다. 10 KB 이상 JSON 메시지가 자주 오가는 브릿지는 직접 호출을 권합니다. 한 앱 안에서 브릿지별로 섞어 써도 됩니다.
dari { }에는 나머지 브릿지만 매핑합니다.- 무거운 브릿지에는
DariInterceptor직접 호출을 둡니다.
이미 DariInterceptor를 호출하는 메서드를 매핑하면 빌드 경고와 함께 건너뛰므로 두 번 기록되지 않습니다. 이 경우 onlyIf는 도움이 되지 않습니다. json() 필드를 먼저 파싱한 뒤에 onlyIf를 확인하기 때문입니다.
런타임: 브릿지 왕복 1회
sample의 debug 빌드에서 BridgeOverheadBenchmark(sample/src/androidTest)로 측정했습니다. 기기는 Samsung SM-S936N, Android 17(API 37)입니다. debug 전용 브릿지(sample/src/debug)를 JavaScript를 거치지 않고 직접 호출하므로, WebView 브릿지 자체 비용은 빼고 캡처 비용만 잽니다.
- 샘플 하나는 요청 호출과 응답 호출 한 쌍입니다.
- 케이스마다 warm-up 100회 뒤 1,000회를 측정했습니다.
- 값은 median / p95, 단위는 µs입니다.
- 세 브릿지는 본문이 같고, 캡처 방식만 다릅니다.
- 캡처 없음: Dari 호출 없음
- 직접 호출: 본문에
DariInterceptor호출 - 플러그인:
dari { }매핑
- JSON 문자열 하나로 받는 브릿지는 호출마다 JSON 메시지 하나를 받고, 분기하려고 직접 파싱합니다. 실제 단일 문자열 브릿지도 이렇게 해야 합니다. 직접 호출은 이미 파싱한 객체를 재사용하고, 플러그인은
json(arg(0), "...")로 매핑합니다.
파라미터를 나눠 받는 형태(arg(n)), dari로 기록
값은 median / p95, 단위는 µs입니다(1,000 µs = 1 ms).
| 페이로드 | 캡처 없음 (µs) | 직접 호출 (µs) | 플러그인 (µs) |
|---|---|---|---|
| 100 B | 1.0 / 1.1 | 238.6 / 432.7 | 178.8 / 226.9 |
| 10 KB | 1.1 / 1.3 | 126.8 / 180.8 | 138.0 / 182.2 |
| 1 MB | 2.0 / 7.7 | 2,692 / 4,013 | 2,736 / 4,092 |
플러그인과 직접 호출의 비용이 같습니다. 시간은 훅이 아니라 항목 기록(복사, 잘라내기, 쓰기 예약)에 듭니다. 10 KB까지는 왕복당 약 0.15–0.25 ms, 1 MB에서는 약 2.7 ms입니다. 100 B 행은 앱 시작 직후 가장 먼저 실행되어서 10 KB보다 느리게 나왔습니다.
JSON 문자열 하나로 받는 형태(json(arg(0), "...")), dari로 기록
값은 median / p95, 단위는 µs입니다(1,000 µs = 1 ms).
| 페이로드 | 캡처 없음 (µs) | 직접 호출 (µs) | 플러그인 (µs) |
|---|---|---|---|
| 100 B | 16.7 / 18.5 | 178.1 / 244.6 | 209.6 / 282.8 |
| 10 KB | 1,603 / 1,754 | 3,039 / 3,313 | 4,771 / 5,462 |
| 1 MB | 208,004 / 258,337 | 364,244 / 494,927 | 696,886 / 782,211 |
작은 메시지에서는 json()도 직접 호출과 비슷합니다. 10 KB부터는 더 비쌉니다. 훅마다 메시지를 한 번 더 파싱하고 data 객체를 다시 문자열로 만들기 때문입니다. 직접 호출은 브릿지가 이미 파싱한 객체를 재사용합니다. 그 차이는 JavaBridge 스레드에서 10 KB에 약 1.7 ms, 1 MB에 약 330 ms입니다. data = arg(0)(원본 문자열)으로 매핑하면 다시 문자열로 만드는 비용은 없어지지만, 다른 json() 필드 때문에 훅마다 한 번은 파싱합니다. 큰 JSON 메시지라면 직접 호출이 더 쌉니다.
dari-noop 사용 시(플러그인 훅은 실행되지만 아무것도 기록하지 않음)
값은 median / p95, 단위는 µs입니다(1,000 µs = 1 ms).
| 페이로드 | 파라미터 분리: 캡처 없음 (µs) | 파라미터 분리: 플러그인 (µs) | JSON 문자열 하나: 캡처 없음 (µs) | JSON 문자열 하나: 플러그인 (µs) |
|---|---|---|---|---|
| 100 B | 0.8 / 0.9 | 8.3 / 8.5 | 30.5 / 80.4 | 18.1 / 82.3 |
| 10 KB | 5.4 / 5.5 | 4.8 / 6.3 | 1,607 / 1,832 | 1,614 / 1,849 |
| 1 MB | 1.1 / 5.9 | 3.0 / 15.3 | 258,411 / 268,779 | 192,969 / 233,727 |
dari-noop을 쓰는 debug 빌드는 호출당 많아야 몇 µs가 듭니다. noop 훅은 아무것도 파싱하지 않습니다. JSON 열의 차이는 브릿지 자체 파싱에서 오는 실행 간 편차입니다. 릴리스 빌드는 아예 변환되지 않으므로 오버헤드가 없습니다.
주의할 점:
- debug 빌드는 AOT 컴파일 없이 실행되므로 절대값은 릴리스보다 큽니다. 의미 있는 것은 열 사이의 비교입니다.
- 1 MB에서는 저장소가 메모리를 빨리 채웁니다. 그 크기의 항목 500개를 버퍼에 두면 기본 256 MB 힙을 넘습니다. 벤치마크는 20회마다 타이밍 밖에서 저장소를 비웁니다.
빌드 시간
MacBook Pro(Apple M2, 8코어, 16 GB, macOS 27)에서 :sample:assembleDebug를 측정했습니다. Gradle 9.1.0, AGP 9.0.0, Kotlin 2.2.10, JDK 21이고, daemon은 워밍된 상태, build cache와 configuration cache는 꺼져 있었습니다. "clean"은 ./gradlew clean 뒤 모든 모듈을 다시 빌드한 것이고, "증분"은 브릿지 클래스에서 한 줄을 바꾼 뒤의 빌드입니다. 값은 median이고, 괄호 안은 범위입니다.
| 빌드 | 플러그인 없음 | PROJECT scope(기본값) | ALL scope |
|---|---|---|---|
| clean | 52.4초 (29.9–78.4, 6회) | 24.9초 (18.8–68.0, 6회) | 139.1초 (89.8–508.8, 3회) |
| 증분 | 3.9초 (3.8–4.5, 3회) | 3.8초 (3.1–8.6, 3회) | 측정 안 함 |
PROJECT scope로 늘어나는 빌드 시간은 측정되지 않습니다. 프로파일한 clean 빌드에서 변환 태스크(:sample:transformDebugClassesWithAsm)는 0.13초였고, 같은 빌드의 :sample:compileDebugKotlin은 5.8초였습니다. clean median 차이는 플러그인이 아니라 실행 간 편차입니다. 일부 측정 중에 다른 프로젝트의 Gradle 빌드가 같은 Mac에서 돌았고, 두 범위는 겹칩니다. ALL은 앱의 모든 AAR을 다시 dex 변환하므로 확실히 느립니다. 508.8초 회차는 다른 빌드 3개와 겹쳤습니다. 이전 테스트에서 Gradle 캐시가 비어 있을 때 ALL은 약 5분 더 걸렸습니다.