Repository navigation
Compress images natively, decoding each image once - #12363
Merged
Merged
Conversation
Contributor
|
The Pull Request introduced native fingerprint changes against the base commit. Generated by PR labeler 🤖 |
Contributor
|
mozzius
marked this pull request as ready for review
November 9, 2026 14:31
Contributor
|
No actionable defects found. Claude review, run |
2 of 6 tasks
Base automatically changed from
samuel/app-3190-limit-image-compression-concurrency
to
main
November 9, 2026 16:56
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mozzius
force-pushed
the
samuel/app-3190-native-image-compressor
branch
from
November 9, 2026 16:56
5813ea3 to
40786b5
Compare
Contributor
|
I didn't find any actionable defects in this PR. Claude review, run |
estrattonbailey
approved these changes
Nov 9, 2026
estrattonbailey
left a comment
Member
There was a problem hiding this comment.
Let's feature flag this and give it a shot
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
arcalinea
temporarily deployed
to
samuel/app-3190-native-image-compressor - social-app PR #12363
November 9, 2026 18:45 — with
Render
Destroyed
Contributor
|
No actionable defects found. Claude review, run |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #12362 (compress at most two images at once), which has merged.
Summary
On iOS and Android, compressing an image for upload used to take one expo-image-manipulator pass for every quality it tried. Each pass decoded the full image from disk again, resized it, encoded a JPEG and wrote it to a file. JS then measured the file and picked the next quality. The binary search keeps going until the quality is pinned down to 1 point, so it takes up to 7 passes per image, and more when it has to shrink the image.
This adds a local module,
expo-bluesky-image-compressor, that does the whole job in one native call:CGImageSourceCreateThumbnailAtIndexon iOS,inSampleSizeplus one scaling draw on Android. The EXIF orientation is applied at the same time.maxDimension, so a 1000px image no longer wastes attempts on "sizes" of 4000, 3200 and 2560px.The output has no metadata (so no location) and keeps the colour profile, so Display P3 photos stay P3. Transparency is flattened onto white on both platforms. On iOS the module runs on its own queue, because by default every Expo module's async functions share one serial queue and a long compression would hold up the others.
Web is unchanged. The manipulator loop moved to
compressWithManipulator.ts, which web uses and native falls back to when the gate is off (see below). The cap of two concurrent compressions from #12362 stays, because a decoded 12 MP photo is still ~48 MB.QualitySearchis duplicated in Swift and Kotlin, and the module README describes the strategy once. I kept the search in native code rather than driving it from JS, so each image costs one bridge call instead of one per quality tried.Feature gate
The native compressor is behind
image_uploads:native_compression:enablein GrowthBook, in the App project.Results
On device
I compressed 10 images with the post settings (4000px, 2 MB) on an iPhone 14 and a Galaxy A10 (2 GB RAM, 32-bit):
"Manipulator loop" is what #12362 runs on native, now in
compressToFit.web.ts. Both use a cap of two, as the app does. There are 3 runs of each on each phone.iPhone 14
Galaxy A10
The failed manipulator run was killed by Android's low-memory killer at 928 MB, about 4 s in, before any image finished. On an earlier try with a development JS bundle, which starts from a higher baseline, native ×2 finished both runs and the manipulator loop was killed both times. So #12362 on its own leaves 2 GB Android phones right at the edge.
Without the cap:
Native with all 10 at once still gets killed on the A10, so the cap from #12362 is still needed.
Output. I pulled the files off both phones and checked them:
Memory per image. Each compression on iOS briefly holds about 130 MB, measured with the same code on macOS. ImageIO decodes a 12 MP photo at full size and then scales it to 4000px, and the encoder keeps its own copy. That memory comes back after every image. Encoding straight from the file could lower it, but that would be a separate change.
Method. A temporary overlay in the app, not committed, downloaded the test images into the cache and timed each compression. The app ran a production JS bundle (
expo start --no-dev --minify) and was relaunched before every run. Memory was sampled every 50 ms:phys_footprinton iOS, which is what the iOS out-of-memory killer (jetsam) counts, and RSS on Android. Treat the A10 memory figures as rough, because under pressure it swaps pages to compressed RAM (zram). The kills are the clearer signal there.macOS harness
ImageCompressor.swiftonly depends on ImageIO, so I also compiled it on macOS and ran it on a few more inputs at the same settings:Test plan
This adds a native module, so it needs a new build and can't go out as an OTA update.
Compressed imagewith a quality../gradlew :bsky-app-expo-bluesky-image-compressor:compileDebugKotlinxcodebuild -scheme ExpoBlueskyImageCompressor(simulator)pnpm typecheck,pnpm lint,pnpm testPart of APP-3190.
🤖 Generated with Claude Code