# Paper Finder: A Free AI-Powered Research Tool and the 7-Second Freeze I Had to Debug

> Source: <https://dev.to/emmanueladu/paper-finder-a-free-ai-powered-research-tool-and-the-7-second-freeze-i-had-to-debug-a2l>
> Published: 2026-07-29 02:40:52+00:00

I built [Paper Finder](https://paperfinder.dev), a free tool that searches arXiv, Semantic Scholar, and Crossref at once, then re-ranks the results by semantic relevance.

The ranking uses a small AI model that runs entirely in the browser, so there are no API costs or accounts. But it introduced a nasty bug: typing in the search box could freeze for up to seven seconds.

Paper Finder initially sorted results by year. To improve relevance, I added `Xenova/all-MiniLM-L6-v2`

through transformers.js. It embeds the query and each result, then ranks them using cosine similarity.

The results appeared before re-ranking began, so I assumed the AI work was safely happening "in the background."

Then a user sent me a Chrome DevTools trace showing 1,350.9 ms of input delay. My first thought was, "That can't be the AI—it's async."

I was wrong.

I used Chrome's `PerformanceObserver`

with the `longtask`

entry type, which reports tasks that block the main thread for more than 50 ms:

``` js
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`Blocked for ${entry.duration}ms`);
  }
});

observer.observe({ entryTypes: ["longtask"] });
```

After clearing the browser's model cache and running a fresh search, I saw this:

```
{ duration: 7079, start: 13735 }
```

One task had blocked the main thread for 7,079 ms. During that time, the page couldn't process typing, clicks, or anything else.

`await`

didn't help
The embedding call used `await`

, but that doesn't move work off the main thread. It only yields while waiting for genuinely asynchronous work, such as network requests or timers.

WASM inference is synchronous CPU work. Wrapping it in a Promise changes how you receive the result, not where the computation runs. The model initialization and inference were still competing with rendering and input on the main thread.

I moved the model into a dedicated Web Worker:

``` js
// embedder.worker.ts
self.onmessage = async (event) => {
  const extractor = await getExtractor();

  const output = await extractor(event.data.texts, {
    pooling: "mean",
    normalize: true,
  });

  self.postMessage({ vectors: /* ... */ });
};
```

The main thread now sends text to the worker and waits for the vectors to come back. The public interface didn't need to change—a Worker-backed Promise looks the same to callers.

After repeating the cold-cache test:

```
longtasks: []
```

Zero long tasks, and the search box remained responsive throughout.

The main lesson: `await`

does not mean "won't block the main thread." For client-side ML or any other CPU-heavy work, use a Web Worker from the start.

The source is [on GitHub](https://github.com/emmanuel-adu/paper-finder), and the live app is at [paperfinder.dev](https://paperfinder.dev).
