Browser Engine Patcher

Home/Blog/Patch approaches

Branches, hooks or diffs: three ways to patch an engine

2026-06-11Design10 minutes

Every team that needs a modified engine picks one of three approaches, usually by accident. We maintained all three at various points between 2025 and 2026, and each one fails in a place that tells you whether it fits your team.

Approach one: a long-lived fork branch

Clone the engine, commit your changes, rebase onto each upstream release. The attraction is that it uses tools everyone already knows and keeps history. The failure arrives at the first rebase after six months of changes: forty commits, three of them named “fix build”, and no way to tell which one causes a test to fail after the move.

$ git rebase upstream/main
# 14 conflicts in 9 files
# resolved by hand over two days
$ git log --oneline | head
8f1c4a2 fix build again
0c9b7e1 fix build
...

A branch also makes review hard for anyone outside the team. The diff of a fork against upstream includes every upstream change since the fork point, so a reviewer sees thousands of unrelated lines around the twelve that matter. We tried tagging our own commits and diffing tags; it works until somebody rebases and the tags point at new objects.

Approach two: runtime hooks only

Do not touch the source. Ship an extension or an injected library that changes behaviour at runtime. This is genuinely the best option for a large class of changes: intercepting a request, adjusting a rendered tree, adding a panel to a developer tool. It requires no build, so it tracks upstream automatically and never conflicts.

It fails for anything decided at compile time. If a feature is behind a build flag, the code that implements it may not be in the binary at all; no hook can call it. The same applies to code paths removed by the optimizer and to debug information. We measured one case where a runtime hook could hide a UI element but could not stop the corresponding 11 MB of renderer code from being compiled and shipped. For a team whose goal is a smaller local build, that is a fatal limit.

Approach three: a directory of diffs

Keep the engine checkout clean and apply a short list of unified diffs, each with a header describing its base, dependencies and target files. The checkout is disposable; the series is the artefact you version, review and test.

patches/
  series.toml
  0100-build-flags.patch
  0200-debug-hooks.patch
  0300-devtools-extensions.patch

The advantage shows up in review. A pull request that adds one patch is one screen of diff plus a header, and the reviewer can apply it to a clean tree with one command. The lockfile gives the whole combination a name, so a bug report says “series 4c1e, revision 8f2c41d9” instead of “my build”.

The cost is discipline. A patch must have one purpose, or it becomes a branch in miniature. A patch must state its base, or the three-way merge invents context. A patch must be re-tested when upstream moves, and no tool can decide for you whether the moved code still does what you meant.

How they fail

ApproachFirst failure modeRecovery cost
Fork branchRebase conflicts pile up; history stops explaining the changeDays per upstream release once the branch is old
Runtime hooksA change needs compile-time code that is not in the binaryRewrite the change as a build patch, if the flag exists at all
Diff seriesPatches drift from upstream and conflict one by oneMinutes per patch, bounded by the number of patches

The recovery cost is the useful column. A branch hides its conflicts until you rebase, then presents them all at once. A series surfaces a conflict on the patch that caused it, which is usually one file and one hunk.

What we do not claim

A diff series is not automatically reproducible. We had a directory of patches for eight months before the lockfile existed, and during that time two builds from the same series could differ because one of them had an edited patch that nobody committed. The format made the problem visible; the lockfile solved it. Read the post on reproducibility for the rest.

Nor is a series the right answer for a team of one who changes two lines twice a year. A local branch and a note in a text file is cheaper, and no tool should talk you out of the simplest thing that works. The series starts paying for itself when more than one person builds, or when the same series has to produce the same result on a machine you do not own.

Choosing

Write down the changes you need. If all of them are runtime, ship a hook and stop reading. If more than two are compile-time, and more than one person builds, use a series: the format is described in Patch format, and the built-in catalogue gives you working examples to copy. If you are somewhere in between, start with a fork branch, keep each change in its own commit, and switch when the first rebase takes longer than a day.