JavaScriptCore—10 Months Later

JavaScriptCore—10 Months Later thumbnail

Ten short months ago we published a blog post showcasing how Lucid shares code between iOS, Android, and the web. In that post, we highlighted a new feature that executed a subset of our shared JavaScript using JavaScriptCore, a WebKit framework for helping JavaScript inter-op with native code.

Today we’re following up on that post to talk about the lessons we learned in the past 10 months, as well as the eventual changes we ended up making.

So what’s changed? For starters, we’re not using JavaScriptCore anymore—at least not directly. The original blog post outlined all sorts of reasons why JavaScriptCore felt like the right choice at the time and, for some use cases, I think that still may be the case.

For an application as complex as Lucidchart, performance is absolutely critical. For example, we added a new feature using JavaScriptCore that was able to run merge logic between two Lucidchart documents. For some documents, that operation was trivial and took only milliseconds to perform. But for larger, more complex documents (like the ones commonly built by our real-world users), a merge could take 10-30 seconds.

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.

Joseph, Jeff, and Chloe at WWDC, San Jose, California

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 JavaScriptCore

func 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.

The benefits

After every major change in framework or technology, it’s always valuable to ask the question, “was it worth it?” With all the trade-offs noted above, it might be hard to believe that the move was worth it, but for the average customer, it definitely was. If you recall, document merges could take between 10 and 30 seconds for normal documents. After the transition to WKWebView, we noticed that depending on the operations needed to merge documents, the merges were between 10 and 15 times faster. It’s hard to overstate how big of an improvement that is for the end user. WKWebView took an operation that was so slow that users questioned whether the merge was stuck or not to something so fast and seamless that users rarely even notice anything is happening at all.

It’s that difference in user experience that’s really what all of this is about. When a user is using Lucidchart to get their job done, they shouldn’t need to worry about what the software is doing or how fast it is doing it. Good software hides all of its complexity and lets the user focus on the work that needs to be done. In the instance of document merges and the improvements that were made by migrating to WKWebView, I think we really met that standard and the team delivered a truly excellent experience.

2 Comments

  1. Umang KathuriaFebruary 10, 2019 at 8:46 am

    Hey there!

    I am trying to get started with JavaScriptCore framework in the application. I am facing challenge in using javascript file with ECMA6 features-like const, let, module.export ,etc. The code is not called from the iOS and I always end up getting undefined in the JSValue at iOS side.

    I was wondering if JavaScriptCore supports ES6 and if yes, How would be able to use it ?
    Also do I need to bundle my javascript file using something like a webpack inorder to use it with JavaScriptCore ?

    Any help is appreciated!

    Thanks alot!

  2. Joseph SlinkerApril 15, 2019 at 3:47 pm

    It’s important to remember that the version of JavaScriptCore available to developers is the same as what is used in Safari. That means that for ES6 support you can check and see what Safari for your iOS version supports and base it off that.

Your email address will not be published.