Jeremy Nwachukwu // Field notes

The Pastor Complained: How I Used an AI Agent to Diagnose and 10x Our App’s Search Speed

103 views October 4, 2026

In my church, we recently switched from EasyWorship to a lyrics display app for our lyrics. It offered some quick wins nobody can deny , open source, works in any browser, easy to pipe into vMix for full control over what's displayed. But one thing it didn't have was Bible verse support, and for a church, that's basically a show-stopper. So I got put in charge of adding it.

I built the feature with an AI agent, and it ended up slow as hell , slow enough that we got real complaints from the pastor about the delay. So I had to fix it, with the same agent. Before you ask , no, I wasn't using Claude or a similar high-end model, I was using Gemini 3 Flash. The process taught me a lot about user experience and browser behavior I genuinely didn't understand before.

What the old logic looked like

Here's how the old version worked: when you typed into the search box, it searched for Bible verses stored in localStorage. The Bible XML files had already been parsed into JSON and stored there, and , bad call on my part , every search checked every translation you had installed, not just the active one. On top of that, each search walked through every book, then every chapter, then every verse, checking for a match.

Bible book Bible Chatper verse

So performance depended entirely on how many books, chapters, and verses it had to crawl through. The more there was, the slower the search.

How I fixed it

Reading back through that, there are two clear problems. First: localStorage isn't async. Every time the Bible store updated, it blocked the main thread, which made the whole app feel sluggish. So the first fix was migrating to IndexedDB. I was honestly a little scared of IndexedDB going in , from what I'd heard, people use it as a local cache that syncs to a cloud database, and that sync layer is where most of the horror stories come from. But in my case, I'm only using it for local Bible storage with no sync involved, and it worked great.

Second problem: I was searching on every keystroke. The idea was that I didn't want the user to notice any delay while typing , but that was naive. Firing off a new search request before the previous one had even finished meant most of those searches never even got used. The fix was adding a 300ms debounce. In the old architecture, this mattered doubly, because every search was also blocking the main thread via localStorage in a loop, so rapid-fire searches while someone was still typing compounded the freeze.

That brings me to the next issue, which deserves its own explanation: in the old architecture, Bible search ran entirely on the main thread. That was a bad call on my part, because every search blocked the UI , the user would pick a verse, and then localStorage itself would also stall things. We fixed the localStorage side with IndexedDB, but that still left the search itself blocking. So the obvious move was to push the search into a Web Worker.

Here's the catch, and the second reason searching on every keystroke was a bad idea: if you spin up a new Web Worker on every single keystroke, you overwhelm the CPU immediately. The fix isn't "don't use workers" , it's "don't create a new one per keystroke." Set up one worker when the Bible loads, and just send it messages after that.

The real fix: the search algorithm

Before I get into how the new algorithm finds verses, quick refresher on Big-O notation , how we measure how an algorithm's time cost grows as the input grows:

  • O(1) , constant time. Like walking into a room and grabbing the first thing you see. Always fast, regardless of how much is in the room.
  • O(log n) , logarithmic time. Like looking for a word in a dictionary. You open to the middle, see if your target comes before or after it alphabetically, and throw away the half that doesn't match , then repeat. Each step cuts the remaining search space in half. This is the idea behind binary search.
  • O(n) , linear time. Like being told to walk into a room and check every single item one by one until you find what you're looking for. The time this takes scales directly with how much is in the room.
  • O(n log n) , linearithmic time. Like tidying a messy room by repeatedly splitting it in half, organizing each half, and merging the results back together. The "halving" part gives you the log n, but you still have to do that work across the whole n items, so the two combine.
  • O(n²) , quadratic time. The classic case: two lists, and for every item in the first list, you loop through the entire second list to compare it. That nested loop is where the squared term comes from.

The problem with the old search was that it was O(n). It was decently fast for small books, but it really slowed down on big ones , Psalms especially, since it has 150 chapters and over 2,400 verses. Because the algorithm walked through every book, then every chapter, then every verse, the amount of work scaled directly with how many verses it had to touch. Compare that to 1 John, which has 5 chapters and only 105 verses total , a search confined to a book that size does a fraction of the work a search touching Psalms does. Put those two side by side and the O(n) problem becomes obvious.

So before I explain the new algorithm: how many of you know what a database does when reads start piling up? It builds an index. That's the same idea here. Instead of scanning every verse, the new version does a lookup against a pre-built index that maps words directly to the verses containing them. In theory, hash collisions can happen and cause issues, but in practice, this runs at practically constant time , a lookup takes roughly the same amount of time no matter how large the Bible dataset gets.

Conclusion

I haven't really talked about how I used the agent for this, so let me close with that. I used the agent mainly for investigation , proposing theories about where the slowdown could be coming from, pointing at the Bible search algorithm specifically, and pulling in research from the web to confirm or rule things out.

I actually knew about IndexedDB already from watching Theo talk about how it made T3 chat app fast on the client side. That's basically the same pattern I used here. It made a real difference, since there's no more blocking on the main thread , before, several things could collide and stall the app at once.

The old search wasn't terrible, but it got genuinely bad as the dataset scaled up. Now it runs in practically constant time , not literally O(1) in every case, but close enough that performance stays decent no matter the size of the Bible being searched.

Debouncing solved two different problems depending on the architecture: in the old setup, it stopped the main thread from getting hammered by overlapping searches; in the new setup, it stops the app from spinning up more worker overhead than the CPU can handle. That matters because most people aren't running top-tier hardware, and this all has to stay smooth while actively displaying on a screen , we can't afford visible hiccups mid-service.

In the end, this is how I actually use agents: I bring the ideas, the suspicions, and the direction, and the agent helps investigate and build it out. The assumptions were mine, including the naive ones, but working through them together is what got the app fast , and got us the compliments instead of the complaints.

© 2026 Ifeanyichukwu Jeremy Nwachukwu // Tactical Terminal