Skip to content

Patch Performance

Shorebird uses its own fork of Flutter.

Our fork works exactly as Google’s does, including passing all the same functionality and performance tests.

Using Shorebird’s fork should not result in any change in your app. If you ever see any change, please let us know so we can work with you to diagnose!

Updating a Flutter app in production is unique to Shorebird’s fork.

When using Shorebird your app has two different modes of running, un-patched (“release”) and patched (“patch”). Both release and patch builds of your app should function identically to as they would without Shorebird. The only caveat is on iOS where patched builds can sometimes run slower than release builds due to iOS restrictions.

The iOS App Store requires all app update systems (like Shorebird’s) to use an interpreter. An interpreter is a software program that can run other programs rather than running the program directly on hardware. Interpreters are slow.

To work around the performance limitations of an interpreter, Shorebird’s updating system is designed to avoid using the interpreter as much as possible. When you make a “patch” with Shorebird, Shorebird examines the change and attempts to set up the “patch” to only run the new or changed code within the interpreter (slow), and thus leave all un-changed code running on the CPU (fast).

Most of the time when you make a change to a part of your program, only that part of the program is changed. However sometimes small changes to one part of the program can produce large changes in (seemingly) unrelated parts.

This is due to how compilers (programs that turn source code into machine code) optimize their output. Dart’s compiler uses several tricks to optimize programs including “type flow analysis” and “inlining”, both of which can cause these unexpected non-local changes.

As an example, imagine you have a part of your program:

bool isEven<T extends int?>(T value) => value?.isEven ?? false;
void foo(int x) {
print(isEven(x));
}

And you change it to add:

bool isEven<T extends int?>(T value) => value?.isEven ?? false;
void foo(int x) {
print(isEven(x));
}
void bar(int? x) {
print(isEven(x));
}

In that example, isEven and foo didn’t change. So you would expect Shorebird should be able to run both of those on the CPU (not have to use an interpreter). However this is unfortunately not the case. In the “before” case, because isEven was never called with nullable values Dart optimized away all of the null checks in the compiled isEven code. (That’s a good thing.) Unfortunately that means that when the new code now used isEven in a nullable context the compiled contents of “isEven” change, to include null checks. Since the contents of isEven changed, Shorebird will conservatively assume that the behavior of all callers of isEven might have changed as well and we will not use the CPU versions of those either. Thus if your change (however small) might happen to have caused the Dart compiler to reconsider optimizations for some other critical section of code, tiny changes can sometimes cause large sections of your application to “un-link” and run in the interpreter.

Avoiding Performance Changes in iOS patches

Section titled “Avoiding Performance Changes in iOS patches”

Unfortunately there is currently no good way to predict if a patch will cause a performance change to your application.

The good news is that this behavior is relatively rare. 9 out of 10 patches do not run into unexpected non-local changes like this and exhibit no difference after patching. Furthermore, when performance impacting changes occur, Shorebird’s tooling and console will warn you. The bad news is that predicting when a change will trigger the Dart compiler to cause non-local changes to your program is very difficult.

When these non-local changes occur, the best recommendation we have at this time is to try to make a new, smaller diff, and patch again.

For the vast majority of patches there is no performance difference at all. For those which do see a difference, sometimes that difference may still be worth the trade-off as a stop-gap between now and when a user can get an update via the App Store.

We have several approaches to explore to remove this limitation on iOS and intend to continue to improve this behavior in the future.