PatchWise | Making app deployments as fast as web

Bhavya Rawal
Life at Quizizz
Published in
4 min readMay 8, 2024

Quizizz app engages 15 million users worldwide with gamified learning experiences, offering interactive quizzes across various subjects for students to join with their class, study independently, challenge friends, and participate in training sessions or surveys, fostering both educational and professional growth.

Problem Statement

Developing instant app deployment capability for seamless app updates.

Why

Historically, we’ve deployed app updates via native releases, a process that roughly takes 1+ week to reach full user coverage (including review and phased rollout).

Challenges with this approach include:

  • Slow adoption
  • Delayed go-to-market (GTM)
  • Inability to deploy hot fixes promptly

Solutions

One straightforward solution is employing an OTA update system, with Codepush standing out as a popular choice for React Native applications.

Challenges with CodePush:

CodePush downloads the entire updated JS bundle during updates, which presents a challenge with our sizeable 15MB bundle (We’re on it, reducing that hefty 15MB bundle like a champ!)
This large size makes it less than ideal for mandatory app updates due to the extended download times on slower networks.

PatchWise

We implemented a more efficient approach leveraging differential updates. This method involves generating patches by comparing the new JS bundle with the previous one and downloading the generated patch instead of the entire JS bundle on the user’s device.

We are using BsDiff and BsPatch utilities to generate and apply the patches. Further reading on these tools here.

Patch generation workflow

Github Action for creating Native releases:

In our GitHub Action for native releases, we tweaked the setup to handle uploading packaged JS bundles to our S3 bucket. This action also makes an API request to store the native release details alongside the bundle in our mongo collection.

Github Action for creating Patches:

We created a GitHub Action to oversee patch creation. This action generates the new JS bundle and makes an API request to our servers. The API utilises the packaged JS bundle obtained from the previous step and the latest JS Bundle to generate the patch through BsDiff. Additionally, it computes an MD5 checksum for the new JS bundle and stores these details in our MongoDB collection.

Since patches exclusively accommodate pure JS changes, we implemented a check within our API, employing octokit, to validate the absence of native changes between the two commits (from native release to patch release commit).

The generated patch is then rolled out to the users.

Patch application workflow

The generated patches are subsequently applied to the downloaded bundle on the user’s device during runtime utilising BsPatch. This is done on the Splash screen before loading the React activity.

static public void setJSBundle(ReactInstanceManager instanceManager, String latestJSBundleFile, Context context) throws IllegalAccessException {
try {
JSBundleLoader latestJSBundleLoader;
if (latestJSBundleFile.toLowerCase().startsWith("assets://")) {
latestJSBundleLoader = JSBundleLoader.createAssetLoader(context, latestJSBundleFile, false);
} else {
latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile);
}

Field bundleLoaderField = instanceManager.getClass().getDeclaredField("mBundleLoader");
bundleLoaderField.setAccessible(true);
bundleLoaderField.set(instanceManager, latestJSBundleLoader);
} catch (Exception e) {
throw new IllegalAccessException("Could not setJSBundle " + e.getMessage());
}
}

public void startReactActivity(String URL) {
try {
BundleDiff.setJSBundle(instanceManager, URL, getApplicationContext());
} catch (IllegalAccessException e) {
Log.e(TAG, "Exception - setJSBundle failed default bundle will be loaded - " + URL);
callMainActivityIntent();
throw new RuntimeException(e);
}

instanceManager.createReactContextInBackground();
Intent intent = new Intent(Splash.this, MainActivity.class);
startActivity(intent);
}

Moreover, we’ve integrated an MD5 checksum verification mechanism for the latest bundle stored on the user’s device, cross-referencing it with the checksum generated in the backend. Upon successful validation, the latest bundle is loaded; otherwise, the application falls back to the packaged bundle.

We implemented the entire logic within an IntentService, configured with a 5-second timer. If the download patch and BsPatch operations exceed this time limit, we trigger the React Activity and execute the update in the background within the IntentService. Subsequently, the updated JS bundle is seamlessly loaded upon subsequent launches.

With this approach, our patching process simplifies to creating a pull request to the release branch, initiating the patch workflow, testing the patch on the debug build, and finally deploying it to users.

Limitations:

  • Incompatibility with native changes: Like other OTA update systems, only pure JS changes can be patched.
  • Vulnerability to contractual changes in native APIs across different patch versions.

Results:

  • Update size reduction from 15 MB (CodePush) to 150 KB.
  • App update download time decreased from 1 minute to instant (on a 1 Mbps connection).
  • Reduction in app update adoption time from 2–3 weeks to 3 days, resulting in faster GTM and instant hot fixes.
  • Implementation of mandatory app updates due to the small download size.

Recent example of effectiveness of PatchWise

Swift deployment of a patch release promptly resolved a critical crash. The seamless adoption of the update upon app launch eliminated the issue, contrasting with native releases requiring manual updates to fix persistent crashes.

The App team working behind-the-scenes at Quizizz!

--

--