WKWebView Communication Latency #
One of the many exciting things about the modern WebKit API is that it has an officially sanctioned communication mechanism for doing JavaScript-to-native communication. Hacks should no longer be necessary to get data back out of the WKWebView
. It is a simple matter of registering a WKScriptMessageHandler
on the native side and then invoking it with window.webkit.messageHandlers.<handler name>.postMessage
on the JS side.
After a heads-up from Jordan that messaging latency with the new web view was not that great, I extended the test bed I had developed to measure UIWebView
communication mechanisms to also measure this setup. I got results that mirrored Jordan's experiences — on an iPad mini 2 (A7), UIWebView
's best officially supported communication mechanism (changing location.hash
¹) took 0.44ms, while the same round trip on a WKWebView
took 3.63ms. I was expecting somewhat higher latency, since there is a cross-process IPC involved now, but not this much higher.
Curious as to what was going on, I pointed a profiler at the test harness, and got the following results.
Running Time Symbol Name 4147.0ms 94.1% Main Thread 10 stack frames elided 2165.0ms 49.1% IPC::Connection::dispatchOneMessage() 2164.0ms 49.1% IPC::Connection::dispatchMessage(…) 2161.0ms 49.0% WebKit::WebProcessProxy::didReceiveMessage(…) 2160.0ms 49.0% IPC::MessageReceiverMap::dispatchMessage(…) 2137.0ms 48.4% void IPC::handleMessage<…> 2137.0ms 48.4% WebKit::WebUserContentControllerProxy::didPostMessage(…) 2132.0ms 48.3% ScriptMessageHandlerDelegate::didPostMessage(…) 1274.0ms 28.9% -[JSContext initWithVirtualMachine:] 769.0ms 17.4% -[JSContext init] 30.0ms 0.6% -[BenchmarkViewController userContentController:didReceiveScriptMessage:] 16.0ms 0.3% -[JSValue toObject]
It looks like nearly half the time is spent in JSContext
² initializers. As previously mentioned, the WKWebView API is open source, so it's actually possible to read the source and see what's going on. If we look at the ScriptMessageHandlerDelegate::didPostMessage
source we can see that indeed for every postMessage()
call on the JS side a new JSContext
is initialized to own the deserialized value.
Even when not doing any postMessage
calls on the JS side, and just measuring WKWebView
's evaluateJavaScript:completionHandler:
latency, there was still lots of time spent in JSContext
initialization. This turned out to be because the completion handler also results in a JSContext
being created in order to own the deserialized value. Thankfully this only happens if a completion handler is specified and there is a result, in other cases the function exits early.
After thinking about this some more, it occurred to me that the old location.hash
mechanism could still work with a WKWebView
. By adding a WKNavigationDelegate
to the web view, it's possible to observe location changes on the native side. I implemented this approach and was pleasantly surprised to see that it took 0.95ms. This was almost 4x faster than the officially sanctioned mechanism (albeit still about twice as slow as the equivalent on a UIWebView
, but that is presumably explained by the IPC overhead).
I then wondered if using location.hash
to communicate in both directions (changing the location on the native side, listening for hashchange
events on the JS side) would be be even better (to bypass more of the JS execution machinery), but that approach ended up being slower (for both UIWebView
and WKWebView
) since it involved more delegate invocations.
Putting all this together, here are the results (in milliseconds, averaged over 100 round trips) using these mechanisms on various devices (all running iOS 8.1) using my test bed.
Method/Device | iPhone 5 (A6) | iPad mini 2 (A7) | iPhone 6 (A8) | Simulator (Core i7) |
---|---|---|---|---|
UIWebView |
||||
location.hash |
0.63 | 0.44 | 0.37 | 0.15 |
WKWebView |
||||
WKScriptMessageHandler |
6.97 | 3.63 | 3.03 | 2.75 |
location.hash |
1.3 | 0.95 | 0.77 | 0.32 |
I reported this performance problem to Apple last summer (while iOS 8 was still in beta), but I haven't seen activity in this area. Ideally there would be a way to skip over JS value deserialization altogether, for cases where the client doesn't actually benefit from the built-in parsing that is done. Specifically, Quip uses Protocol Buffer-encoded messages to have richer (and typed) data passed back and forth between the native and JS side, and so, as far as the JS runtime is concerned, they're all strings anyway.
Separately, it is a good question as to why I'm making such a big fuss over the overhead of one millisecond (or three) per call. Quip continues to be a “hybrid” app, with the editing experience implemented in a web view (a UIWebView
for now). With the launch of spreadsheets we now have even more native ↔ web communication, and in some cases we notify the web view of touch events continuously while UIScrollView
s are scrolled (for custom behavior³). When trying to hit 60 frames per second, spending 3 milliseconds of your 16 millisecond budget on pure overhead feels wasteful.
Update on 06/29/2015: Mark Lam recently fixed ScriptMessageHandlerDelegate::didPostMessage
to reuse JSContext
instances across calls, which fixes one of the two performance problems that this post covers. I've filed another bug to track the remaining issue with evaluateJavaScript:completionHandler:
.
Update on 08/12/2015: The other bug has been fixed too, and both fixes are live in iOS 9. I have also discovered additional alternative mechanisms that perform better. See my more recent post about the current state of things.
- Astute observers will remark that my blog post used to recommend either changing
location.hash
or using theclick()
method on an anchor node. I have since discovered thatclick()
results in a forced layout recalculation (via hit testing) as part of dispatching the event. I have since updated the post to recommendlocation.hash
. - I find it a bit odd that
JavaScriptCore
is not part of Apple's web-accessible documentation set. Even their own release notes announcing the framework say “for information about the classes of this framework, see the header files.” - This involved yet more spelunking into
UIWebView
internals, something I will hopefully be posting about soon.
4 Comments
Haven't tried them myself for UI/WKWebView bridging yet but latency for both should be in low microseconds.
The problem for me, is passing MIDI data from the native app to the synth running in a WebView. UIWebView is what I started with and it didn't perform too badly for the sound part, but scrolling in it would sometimes interrupt sound. I've divided UI and the Synth into 2 separate WebViews and while searching for performance stats for the FamousEngine I came about an article about WKWebView and decided to switch and check it out.
Unfortunately, the delay of passing MIDI events to the Synth makes it unplayable. And I can't even use one view for better performing UI, because of the delay generated between the UI and the native shell.
Do you know of a better way of communication between the Javascript code and the native shell, that will lower latency to UIWebView levels?
In anyways, thanks for posting about this, I honestly didn't expect to find anything about it on the interwebs. :)
Post a Comment