Finding the problem
When the average total time spent in a mobile app is measured in seconds, taking 30 seconds to merge documents is not acceptable. We did our best to instrument and tune our code but quickly came to the conclusion that the problem was JavaScriptCore and not our code. We first became suspicious of JavaScriptCore when we tried running the same merge logic in Safari. When run in Safari, the merge wasnât just fast, it was shockingly fast. Luckily this realization happened in the weeks leading up to Appleâs Worldwide Developers Conference, a place where we have worked with the WebKit team to solve problems in the past.When the week of WWDC came around, we scheduled our appointment with the WebKit team and arrived with test devices in hand, ready to show them our findings. In our meeting, we were very confident that we could demonstrate the performance issues, but what we didnât expect was their response. After briefly explaining what our JavaScript was doing and the issues we were having with JavaScriptCore, the WebKit engineer we met with only had one question: âAre you doing all this on macOS or iOS?â Lucidchart doesnât have a native macOS app, and so this question completely caught us by surprise. For readers that arenât aware, macOS and iOS apps share a lot of their core technologies, including the WebKit frameworks. So this question highlighted a characteristic of the frameworks that we had never considered: JavaScriptCore is different on iOS and macOS. After explaining that the Lucidchart app was only available on iOS, the WebKit engineerâs response was immediate, âOh, yeah, donât use JavaScriptCore on iOS. You should be using WKWebView.â The conversation went on for another couple minutes, but the gist is this: JavaScriptCore on iOS is performance constrained for security reasons. Itâs so obvious when itâs pointed out, but in the heat of performance profiling, we never considered the security model of iOS. On macOS the security model enforced by the system is much looser than on iOS. A prime illustration of this is how you get apps on the two platforms. On macOS an app may come from the App Store, but it may also be downloaded from a developerâs website. On the other hand, iOS apps exclusively come from the App Store and downloading from a website isnât an option. This restriction on what code can be run by iOS apps is reflected in JavaScriptCoreâs restrictions. Because JavaScriptCore runs in the same process as the host app, it doesnât have access to JIT compilation, which in turn heavily cripples performance. Itâs a necessary security feature, but in a performance-sensitive app like Lucidchart, itâs a dealbreaker. Luckily, for developers in the same boat as us, thereâs another way: WKWebView.
Migrating to WKWebView
In theory, WKWebView isnât that much different from JavaScriptCore in terms of JavaScript execution, which makes sense. The purpose of JavaScriptCore was to give developers direct access to the WebKit JavaScript engine. In practice, however, they are very different. WKWebView runs in a separate process from the host app, making it less susceptible to the security concerns mentioned above. That means that WKWebView has access to JIT compilation and doesnât have the same performance limitations that using JavaScriptCore directly has. So what does the migration from JavaScriptCore to WKWebView look like? Take a look at these two equivalent APIs, one from JavaScriptCore and the other from WKWebView: JSContext within JavaScriptCorefunc evaluateScript(String!) -> JSValue!
WKWebView
func evaluateJavaScript(String, completionHandler: ((Any?, Error?) -> Void)? = nil)
There are two major differences between the APIs, and they are both a result of WKWebView running in a separate process from the host app. The first difference is in the return value. Notice how the JSContext version of the API simply returns a
JSValue. If you read our previous blog post you know that JSValue can either represent a JavaScript type or can be materialized into an instance of the native object it represents. Compare that with the Any?, Error? returned from the WKWebView API. Because it is running in a separate process with its own networking, and its own memory space, it returns an optional Error and either a Dictionary or an Array representing the JSON returned by the JavaScript that was evaluated. Itâs unfortunate, but WKWebView doesnât support all of the cross-language translation that made JavaScriptCore so appealing. Itâs not the end of the world, but it is a trade-off to consider.
The second difference between the two APIs is again in the return valueâspecifically the fact that the WKWebView API doesnât have a return value. Instead it takes a closure as a second parameter and uses that to asynchronously return the value from the evaluated JavaScript. This might seem small at first glance, but in practice, it can make code pretty difficult to reason about. As an example, the Lucidchart app loads the necessary JavaScript for the new document merging feature in small ordered chunks. Again, itâs not the end of the world, but loading several chunks of dependent JavaScript using asynchronous APIs is almost impossible to without something like PromiseKit to keep code dependencies obvious.
