---
title: "Patch Performance"
description: "Why a patched iOS app can run some code more slowly than the release, how to tell from the link percentage, and how to avoid it."
---

Shorebird uses its own fork of Flutter.

Shorebird’s 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, reach out on [Discord](https://discord.gg/shorebird) so the Shorebird team can help diagnose.

### Patching

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 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](/code-push/faq/#does-shorebird-comply-with-app-store-guidelines) 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).

### Why iOS patches are sometimes slow

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 won’t 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

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 patch` warns 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 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.

Several approaches exist to remove this limitation on iOS, and Shorebird intends to continue improving this behavior in the future.

You can’t predict it from the change, but you can measure it once the patch is built: check its link percentage in CI, as described in [Link percentage and slow patches](#link-percentage-and-slow-patches).

### Link percentage and slow patches

Read this if `shorebird patch ios` or `shorebird patch ios-framework` warned that it could only share part of your Dart code with the released app, refused to publish because the link percentage was below the minimum, or your app runs slower after a patch.

#### What link percentage means

When Shorebird builds an iOS patch, it links the patch against the release. Code the linker can match to the release keeps running on the CPU. Changed code, and code that depends on it, runs in the interpreter. The link percentage is the share of the patch’s Dart code that was linked this way.

A patch that changes a few functions usually links at close to 100%. Below 90% is worth investigating. The usual causes are a patch that contains more change than you meant to ship, or one built differently from the release. Sometimes a small change unlinks a lot of code on its own, as described in [Why iOS patches are sometimes slow](#why-ios-patches-are-sometimes-slow). A low percentage only slows your app down if the unlinked code is code your app spends its time in.

Link percentage applies to `shorebird patch ios` and `shorebird patch ios-framework`. Android, macOS, Linux and Windows patches are not linked and have none.

#### The warning below 90%

When the link percentage is below 90%, `shorebird patch` warns:

```
shorebird patch was only able to share 70.2% of Dart code with the released app.
This is unexpected, and means the application may execute slower than expected after patching.
```

The patch summary also names the debug file:

```
🔍 Debug Info: /path/to/your_app/build/patch-debug.zip
```

The patch still publishes. Check the [common causes](#common-causes) below. If none applies, publish to a [staging track](/code-push/guides/staging-patches) first and check the app’s performance before promoting the patch.

#### The failure below 20%

In 1.6.135 and newer, `shorebird patch` refuses to publish a patch that links below 20%:

```
The link percentage of this patch (12.345678901234567%) is below the minimum threshold (20%).
Not publishing. Lower --min-link-percentage to accept this patch, or see /path/to/your_app/build/patch-debug.zip for what could not be linked.
```

The check also runs with `--dry-run`, so a dry run fails the same way. Patches this far below normal usually come from one of the [common causes](#common-causes) below, so look there before lowering the threshold. To publish the patch anyway, pass a lower `--min-link-percentage`. `--min-link-percentage=0` accepts any link percentage, as older CLI versions did.

#### Check the link percentage in CI

`--min-link-percentage` also works the other way: raise it to stop a patch that links worse than you expect before it reaches users.

```
shorebird patch ios --release-version=1.0.0+1 --track=stable --min-link-percentage=95
```

To check a patch without publishing it, for example on every pull request, add `--dry-run`. In 1.6.134 and older, a dry run exits before the link percentage is checked, so this needs 1.6.135 or newer:

```
shorebird patch ios --release-version=1.0.0+1 --track=stable --min-link-percentage=95 --dry-run
```

See `shorebird patch ios --help` for the other options.

#### Common causes

* **The patch contains more change than the fix.** Upgraded dependencies (`pubspec.lock`), regenerated code and refactors all change compiled code outside the fix. Build the patch from the release’s commit plus the fix. Even a small change can unlink code that calls it, as described in [Why iOS patches are sometimes slow](#why-ios-patches-are-sometimes-slow).
* **The patch is built with different settings from the release.** Pass the same `--flavor`, `--target` and `--dart-define` (or `--dart-define-from-file`) values you released with. Shorebird builds the patch with the release’s Flutter version and obfuscation setting for you, except in the case below.
* **No `--release-version` in CI, on 1.6.136 and older.** When `shorebird patch` has no `--release-version` and can’t prompt, it builds the app to find the release version and can reuse that build as the patch. That build doesn’t apply the release’s obfuscation, so a patch for a release built with `--obfuscate` links poorly. Always pass `--release-version` in CI.
* **Two builds at once in the same project, on 1.6.134 and older.** Running `shorebird patch ios` while another build, such as `shorebird patch android`, runs in the same project could build the iOS patch from the other build’s Dart code, which links very poorly. Update the CLI with `shorebird upgrade`, or run the builds one after the other.

#### Getting help

If none of these explain a low link percentage, send Shorebird `build/patch-debug.zip` with your app ID and release version. It contains your compiled Dart code, so read [Sending a patch-debug.zip to Shorebird](/code-push/troubleshooting/#sending-a-patch-debugzip-to-shorebird) for what is inside and how to send it privately.
