Recently while building my iOS document reader app, I relearned a good lesson: You’re not done until you’ve successfully used your software on the least capable possible hardware, not the most.
If you want to write fast software, use a slow computer
Testing on a slow iPad saved me a lot of headaches later – the issues the iPad surfaced within a couple of uses wouldn’t have emerged on a new iPhone until later. The tests helped me uncover several serious bugs.
Plus, where without the testing I might have concluded later that the iPad’s failure to work with my app was just due to insufficient hardware, instead I got an app that worked extremely well even on old hardware. In the universe where I didn’t try the old iPad, I can imagine users reporting their iPad couldn’t run the app, and I’d dismiss it with a “buy a newer iPad,” when it actually ought to work.
As a developer, it should make sense to get your project working smoothly on the slowest performing hardware you expect your software to support, not the fastest. Developers often have the latest Apple hardware and don’t realize they’ve built something that will crawl for the majority of their users.
Of course, you may have constraints like very slow builds or special limitations on where you can build, and in those cases you should prioritize testing on an old machine as the next best option. For instance, I developed an iPhone/iPad app and to do so I had to use my M4 Mac Mini to build – iOS development is an unusual case where you can’t test and build on the same type of hardware. (In case you don’t know, Apple has a selection of iPhone simulators in Xcode on macOS so you can do a lot of your functional testing there without loading your app on a second device just to test, but you will still miss performance specific issues.)
My iOS Experience
My “Doc-Assist” app lets users quickly scan and catalog many documents or other objects with print, and do so hands-free (the device must be mounted over a desk). It can replace the need for a document camera in many cases. It’s primarily meant for screen reader users who are blind or low vision or with other print-reading disabilities, but it will make life easier for anyone trying to catalog and digitize stacks of documents. For instance saving receipts is a bother, but if you have a dedicated spot on your desk where your old iOS device watches, you just drop a receipt there as you walk by. It will immediately scan, OCR and save it, plus sync to iCloud as a backup. It’s about as easy as a good checkout scanner.
The Doc-Assist app employs on-device Apple OCR, speech recognition (STT) and text-to-speech (TTS). For summaries and document / object descriptions it relies on Gemini 3.5 in the cloud, with Mistral OCR as a check on the Apple OCR if it struggles. You can choose to keep everything on device and skip the Mistral check and the AI summaries. Future iOS 27 versions will have access to small on device AI models and Apple-specific larger models.
As you can imagine, there are a couple of potential bottlenecks: OCR on high-resolution images; realtime monitoring of the scanning surface with a video stream; uploading to the cloud; STT and TTS. Do all this and try not to drain the battery. The realtime video stream monitoring and correctly choosing when to take a high-res photo were the main difficulties.
After a couple of weeks I’d reduced latency to something that felt snappy – a few seconds per document. Most of the rest of the effort went into getting the computer vision module to crop the objects of interest and accurately detect when to read and to detect page flips on documents.
Kind of on a whim I decided to try testing on my 6th generation iPad (2018 vintage). It has an A10 processor (4 cores, but effectively only 2) and 2G RAM, running iOS 17.x. The camera has 8 megapixels, just enough to probably work for OCR-quality photos. My original test device was an iPhone 16 with 8G RAM (the A18 processor, 6 cores) and 48MP camera, running iOS 26. You can capture 10, 12 or 48 MP still photos with the API.
| iPad 6th gen | iPhone 16 | |
|---|---|---|
| Chip | A10 (2016) | A18 |
| RAM | 2 GB | 8 GB |
| OS | iOS 17 | iOS 26 |
| On-device speech recognition | none | yes |
Here’s what the iPad testing revealed, that the iPhone tests had masked:
1. Concurrency weak point: Image capture eventually fails. On the iPad, it failed after the first or second capture.
There was a loop taking 15 frames per second live video at a low resolution for motion and object detection. It’s implemented as a queue of frames coming from the camera, getting processed by our computer vision module. The full resolution captures were scheduled on this same queue and thread, which is fine on the iPhone, but the iPad couldn’t process the queue fast enough and everything got bogged down. It could barely keep up processing the low-res frames. Sometimes the full-res capture timed out.
Since the iPad has two high performance cores this is solvable with parallel processing: Put the high-res captures on their own thread (with a lock). I learned later that part of the reason inserting a full-res photo was extra detrimental to performance on the iPad is that the image processing on the iPad 6gen is way slower than on the iPhone 16 beyond the CPU speed differences.
The core of the app is written in Go, so we had Go’s concurrency / goroutines giving semantic concurrency while processing work from the image queue, but the work was still scheduled on one physical core and that just fell down on hardware that was too slow. I couldn’t profile the app in detail while it ran on the iPad but the obvious move to separate the stills and motion capture goroutines did the trick.
This finding tells us an older, single core iPad would be non-viable for my app.
2. Memory leak: Image captures were progressively slower. Even when they didn’t fail right away they took longer.
The Apple Vision VNImageRequestHandler call (Objective-C) put the full resolution images into an “auto-release” Objective-C buffer. There was never a release to trigger the “auto-release” so the memory just piled up, about 32M to 120M per photo. Go’s garbage collector couldn’t see the memory used in the buffer. Swift’s ARC would mark it as to be released. Swift wraps the main UI thread in an autorelease pool and you never have to think about it; you have to manually use one on a background thread.
The OCR runs on a Go runtime thread. We call into the Swift framework, now calling a helper function that wraps the VNImageRequest call with an autorelease pool, returning just the OCR results.
This problem would eventually break the app on an iPhone 16 too, but over a much longer time than the iPad.
3. Simply very slow image processing for still photos (up to 73 seconds).
The app requests still photos destined for OCR to be quality priority. The computational photography pipeline quality on the iPhone 16 is pretty quick, but on the iPad it is extremely slow. The quality level isn’t really needed, so on an older device we request balanced. Right now the code checks for < 3G to make that choice; there needs to be a better way to select, like camera type.
4. No voice commands available
This is an iOS version problem. We don’t have native on device dictation available through the API we need. There is a cloud option a user can pick now. Modern iPhones can do on device dictation.
Lessons Learned
- Always test with the least capable hardware you expect to actually work.
- Picking Go for the core of the application just complicated things for me. I did it because the app started as a command-line app on the Mac monitoring a detachable web-cam and I wanted it to be cross platform.
The end result after the testing is an app that should work on any two-core or more CPU Apple device running iOS 17 or more with at least an 8MP camera. This covers a whole lot of devices currently out there, making it affordable to almost anyone. I got the old iPad at a shop for $50 for instance. The app runs nearly as well on the iPad as on the iPhone.
A second iOS 27 version of the Doc-Assist app will allow using Apple AI models and better STT, maybe better OCR, and better conversational flow support. iOS 27 capable devices cost a lot more so I won’t retire the first version of the app.