<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Devico Solutions</title>
    <description>The latest articles on DEV Community by Devico Solutions (devico-solutions).</description>
    <link>https://dev.to/devico-solutions</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14571%2Fbaba7ed0-f054-4d76-be34-0b8e01296966.jpeg</url>
      <title>DEV Community: Devico Solutions</title>
      <link>https://dev.to/devico-solutions</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devico-solutions"/>
    <language>en</language>
    <item>
      <title>How to Build a Scalable File-Processing Pipeline: Validation, Metadata Extraction, Queues, and Workers</title>
      <dc:creator>Vic Tekys</dc:creator>
      <pubDate>Tue, 15 Sep 2026 09:35:00 +0000</pubDate>
      <link>https://dev.to/devico-solutions/how-to-build-a-scalable-file-processing-pipeline-validation-metadata-extraction-queues-and-3p33</link>
      <guid>https://dev.to/devico-solutions/how-to-build-a-scalable-file-processing-pipeline-validation-metadata-extraction-queues-and-3p33</guid>
      <description>&lt;p&gt;Here's a failure I've seen more than once on upload-heavy products: the endpoint returns 200, the user closes the tab, and twenty minutes later there's no order in the system because a worker choked on a vendor's proprietary container format. Nobody told the user. Nobody told the worker's author either, because the retry policy quietly redelivered the file until it hit a dead-letter queue nobody monitored.&lt;/p&gt;

&lt;p&gt;The standard advice — "put a queue in front of it" — doesn't prevent any of this. The queue was never the hard part.&lt;/p&gt;

&lt;p&gt;After building ingestion for dental CAD files, construction models, bulk carbon data, and business documents, I've settled on a different frame: a file pipeline is four stages with different latency budgets and different failure semantics. Throughput is decided by how early you reject bad input and how much derived work leaves the request path — not by which broker you pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Four stages, not one queue&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The four stages are gate, ingest, extract, derive.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gate&lt;/strong&gt; is synchronous and cheap: auth, quota, size, extension, magic-byte sniffing. It runs while the user is still watching and its budget is milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingest&lt;/strong&gt; moves bytes into object storage and writes the metadata record. It should touch your API process as little as possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract&lt;/strong&gt; is asynchronous, per-format parsing: opening the container, pulling metadata, validating structure. This is where the real engineering lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derive&lt;/strong&gt; produces the artifacts your product actually reads — thumbnails, previews, normalized records, optimized model formats. Also async, and unlike extract, usually cacheable and re-runnable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reason to name the stages is that the boundaries between them are where you retry, cache, and shed load. A parse failure shouldn't re-run the upload. A thumbnail failure shouldn't re-run the parse. When everything is one "process file" job, every retry repeats all the work and every failure is opaque.&lt;/p&gt;

&lt;p&gt;On the &lt;a href="https://devico.io/success-stories/carbon-management-solution" rel="noopener noreferrer"&gt;Carbon Management Solution&lt;/a&gt; project — bulk emissions data, where a user uploads a single file containing any amount of data that then gets validated and processed — we ran this on serverless AWS with Lambda, SNS, SQS, and DynamoDB, structured so that a certain service is responsible for a certain stage of the process. Stage isolation was the design, not an accident of the stack.&lt;/p&gt;

&lt;p&gt;Honest caveat: service-per-stage multiplies your operational surface. Four services means four sets of alarms, four deploy pipelines, four things to reason about at 2 a.m. At low volume, one worker pool consuming from a couple of queues is the right call. Draw the stage boundaries in code first; split the infrastructure when a stage actually needs independent scaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Validation is a two-tier problem&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The all-async reflex — "accept everything, validate in the worker" — is how you get the silent-failure story from the intro. But the opposite reflex, deep validation in the request handler, is how upload endpoints time out. Validation is two tiers, and the split is not negotiable.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What has to fail while the user is still watching&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Anything a user can fix by picking a different file must fail synchronously: wrong extension, oversized file, empty file, exhausted quota, a MIME type that doesn't match the bytes. If the user learns about these hours later from an email, your gate failed at its one job.&lt;/p&gt;

&lt;p&gt;The gate checks are cheap because they read a few bytes, not the structure:&lt;/p&gt;

&lt;p&gt;import { fileTypeFromBuffer } from "file-type";&lt;/p&gt;

&lt;p&gt;const ALLOWED = new Set(["model/stl", "application/zip", "application/pdf"]);&lt;/p&gt;

&lt;p&gt;const MAX_BYTES = 500 * 1024 * 1024;&lt;/p&gt;

&lt;p&gt;export async function gate(head: Buffer, declaredSize: number, user: User) {&lt;/p&gt;

&lt;p&gt;if (declaredSize === 0 || declaredSize &amp;gt; MAX_BYTES) {&lt;/p&gt;

&lt;p&gt;throw new UploadError("size_rejected");&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;if (await quotaExceeded(user, declaredSize)) {&lt;/p&gt;

&lt;p&gt;throw new UploadError("quota_exceeded");&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;// Sniff real content type from magic bytes — never trust the filename&lt;/p&gt;

&lt;p&gt;const sniffed = await fileTypeFromBuffer(head);&lt;/p&gt;

&lt;p&gt;if (!sniffed || !ALLOWED.has(sniffed.mime)) {&lt;/p&gt;

&lt;p&gt;throw new UploadError("type_rejected");&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The line to look at is the sniffing: content type comes from the bytes, not the filename. Renamed .exe files and mislabeled archives are a when, not an if.&lt;/p&gt;

&lt;p&gt;On &lt;a href="https://devico.io/success-stories/bego" rel="noopener noreferrer"&gt;BEGO&lt;/a&gt;, a German dental CAD/CAM company, we went further and put scenario detection in the order wizard itself: the upload step auto-detects whether the user dropped 3Shape output, exocad output, standalone STLs, or a mixed set, and routes the flow accordingly. That's still gate-tier work — it reads signatures and file lists, not full structure — and it means the user is corrected before the order exists, not after.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What can only fail inside a worker&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Structural correctness of a real-world format is unknowable until you parse it. Whether a proprietary archive decrypts, whether the XML inside references parts that exist, whether an STL mesh is watertight — none of that fits a request budget, and none of it should.&lt;/p&gt;

&lt;p&gt;So the contract with the user changes shape: the synchronous response means "your file is accepted for processing," never "your file is valid." The pipeline then needs a way to deliver bad news — a per-file status the UI polls or subscribes to, flipping from processing to failed with a reason a human can act on. On BEGO, the order platform tracks per-file status through queue processing for exactly this reason.&lt;/p&gt;

&lt;p&gt;The failure mode to design against: sync checks can be spoofed and deep checks can't be rushed. Any team that tries to collapse the two tiers into one ends up with either a slow gate or a lying one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Metadata extraction is format work disguised as infrastructure&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Generic pipeline tutorials treat "extract metadata" as one box in the diagram. In practice it's the majority of the engineering, because it's per-format parsing, and formats are hostile.&lt;/p&gt;

&lt;p&gt;On BEGO the extract stage deep-parses laboratory formats — .3ox, .dentalProject, .constructionInfo, .modelInfo— plus encrypted .bego/.begostl archives with decryption and retry logic, pulling out patient and order metadata, materials, colors, and tooth mapping. None of that generalizes. Every one of those parsers is bespoke work against a vendor format that ships no public spec and changes without notice.&lt;/p&gt;

&lt;p&gt;On &lt;a href="https://devico.io/success-stories/memomeister" rel="noopener noreferrer"&gt;MemoMeister&lt;/a&gt;, a document-management SaaS we built from scratch for the German market, extraction went a step further into classification: workers analyzed the keywords, data formats, and other characteristics of an upload to determine the document type and route it to the right user or group. The file's type came from its content, not its name.&lt;/p&gt;

&lt;p&gt;Two lessons from shipping that kind of extraction. First, content-derived classification is probabilistic — you will guess wrong, so the design must include a manual-correction path, not just a confidence score. Second, budget for vendor formats changing silently underneath you; a parser that worked for a year is not a parser that works.&lt;/p&gt;

&lt;p&gt;This is why "which queue should I use" is usually the wrong first question. The broker is a commodity. The .dentalProject parser is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Get bytes out of your API process&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One flat infrastructure rule: file bytes should not flow through your API. The client uploads directly to object storage; the API's job is to issue the upload authorization, write the metadata record, and enqueue the job.&lt;/p&gt;

&lt;p&gt;BEGO uploads via pre-signed URLs to cloud storage. MemoMeister stored documents on Amazon S3, with workers picking files up after a successful upload. The pattern is the same either way: the API hands out a scoped, expiring write permission and steps out of the data path.&lt;/p&gt;

&lt;p&gt;The shape of it, with a commit step:&lt;/p&gt;

&lt;p&gt;// 1. API issues the upload — after gate checks, including quota&lt;/p&gt;

&lt;p&gt;const key = &lt;code&gt;uploads/${user.id}/${randomUUID()}&lt;/code&gt;;&lt;/p&gt;

&lt;p&gt;const url = await getSignedUrl(&lt;/p&gt;

&lt;p&gt;s3,&lt;/p&gt;

&lt;p&gt;new PutObjectCommand({&lt;/p&gt;

&lt;p&gt;Bucket: BUCKET,&lt;/p&gt;

&lt;p&gt;Key: key,&lt;/p&gt;

&lt;p&gt;ContentLength: declaredSize, // cap what the URL can write&lt;/p&gt;

&lt;p&gt;}),&lt;/p&gt;

&lt;p&gt;{ expiresIn: 300 },&lt;/p&gt;

&lt;p&gt;);&lt;/p&gt;

&lt;p&gt;await db.file.create({ key, userId: user.id, status: "pending_upload" });&lt;/p&gt;

&lt;p&gt;// 2. Client PUTs bytes straight to storage&lt;/p&gt;

&lt;p&gt;// 3. Client confirms; API verifies the object exists, then enqueues&lt;/p&gt;

&lt;p&gt;export async function commit(key: string) {&lt;/p&gt;

&lt;p&gt;const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET, Key: key }));&lt;/p&gt;

&lt;p&gt;await db.file.update({ key }, { status: "uploaded", etag: head.ETag });&lt;/p&gt;

&lt;p&gt;await queue.send({ key, etag: head.ETag });&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The part to look at is step 3. Without an explicit commit, you have no moment at which the upload is known-complete, and your extract stage starts firing on half-written objects.&lt;/p&gt;

&lt;p&gt;Be honest about what this pattern costs: validation now happens after the write. A client can obtain a URL and never call commit, so you need a sweeper for orphaned objects. Quota has to be enforced before the URL is issued, on the declared size, and re-checked at commit. Direct-to-storage upload is still the right default — it just isn't free.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The queue decisions that actually change throughput&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Now the section everyone expects, argued honestly: broker choice matters far less than the policies around it.&lt;/p&gt;

&lt;p&gt;BEGO runs its workflow processing on Redis-backed queues under NestJS and Kubernetes. MemoMeister ran RabbitMQ, with some workers calling third-party services like Google Vision and others monitoring database changes to update document state. Carbon Management Solution used SNS and SQS. Three stacks, one set of decisions that mattered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue per stage, not one queue for everything.&lt;/strong&gt; Extraction backing up shouldn't delay thumbnail generation for files already parsed. Separate queues give you separate backpressure and separate scaling knobs — and keep CPU-bound parsing workers apart from IO-bound workers waiting on third-party APIs, which want completely different concurrency settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visibility timeout longer than your worst file.&lt;/strong&gt; On SQS-style brokers, a message picked up but not deleted &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html" rel="noopener noreferrer"&gt;becomes visible again&lt;/a&gt; after the timeout. Set it shorter than your slowest legitimate parse and you get two workers processing the same giant file, doubling load exactly when the system is already struggling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A dead-letter queue you actually watch.&lt;/strong&gt; Bounded retries, then park the message. An unmonitored DLQ is the silent-failure story again, one hop removed.&lt;/p&gt;

&lt;p&gt;One warning on MemoMeister's DB-watching workers: they worked, but they couple the pipeline to your schema — every migration becomes a pipeline change. I'd reach for explicit events first and treat DB-watching as a legacy-integration tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Idempotency, retries, and poison files&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Standard queues are &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-standard-queues.html" rel="noopener noreferrer"&gt;at-least-once delivery&lt;/a&gt;: the broker guarantees a message arrives, not that it arrives once. Operationally that means every worker you write will eventually run twice for the same file. Plan for it or debug it.&lt;/p&gt;

&lt;p&gt;The fix is idempotency keyed on the stored object — bucket, key, and ETag — checked before the expensive work:&lt;/p&gt;

&lt;p&gt;async function handle(msg: { key: string; etag: string }) {&lt;/p&gt;

&lt;p&gt;const claimed = await db.extraction.tryClaim(msg.key, msg.etag); // unique index&lt;/p&gt;

&lt;p&gt;if (!claimed) return; // duplicate delivery or a lost race — done&lt;/p&gt;

&lt;p&gt;const meta = await parse(msg.key); // the expensive part&lt;/p&gt;

&lt;p&gt;await db.extraction.complete(msg.key, msg.etag, meta);&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Keying on the ETag rather than just the key means a re-uploaded file with new bytes is new work, while a redelivered message for the same bytes is a no-op.&lt;/p&gt;

&lt;p&gt;Then there are poison files — the input that kills the worker itself. The classic is the model that OOMs the process: the worker dies before it can nack or delete, the visibility timeout expires, the message comes back, and the next worker dies too. Your fleet is now a crash loop with one file as the ignition. Defenses are boring and essential: a max-receive count routing to the DLQ, memory limits that fail the job instead of the pod, and a size ceiling in the gate so the 9 GB file never enters the pipeline at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Derived artifacts are the product&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the second assumption worth breaking: the pipeline doesn't exist to store files. It exists to produce cheap-to-read outputs. If the artifact your product serves is still expensive to consume, you optimized the wrong half.&lt;/p&gt;

&lt;p&gt;BEGO generates STL thumbnails at extract time so the order UI never touches a mesh. And the clearest evidence I have is &lt;a href="https://devico.io/success-stories/vitus" rel="noopener noreferrer"&gt;Vitus&lt;/a&gt;, a Danish constructability platform handling .ifc, .nwd, .rvt, .3dm, and other CAD formats for construction companies like Munck, Femern A/S, VINCI, and COWI. Their viewer struggled not because ingestion was slow, but because the format being loaded was heavy.&lt;/p&gt;

&lt;p&gt;The fix was a bundle of read-side changes — migrating models to SVF2, parallel loading, web workers off the main thread, list virtualization, IndexedDB caching. Together they cut &lt;a href="https://devico.io/success-stories/vitus" rel="noopener noreferrer"&gt;model size by 5x and loading time by roughly 2.5x, with multi-model loading 5x faster&lt;/a&gt;. To be precise about what those numbers are: client-side model-loading gains from the whole bundle of changes, not a server-side pipeline benchmark, and no single change gets the credit. But that's exactly the point — the wins came from changing the data format and the loading strategy. Worker count had nothing to do with it.&lt;/p&gt;

&lt;p&gt;The tax on derived artifacts: they need versioning, and a format migration is a backfill, not a deploy. When Vitus moved to SVF2, every existing model needed re-derivation. Stamp every artifact with the version of the deriver that produced it from day one, so "re-derive everything older than v3" is a query and a queue-fill instead of a forensic project.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Constraints that beat the ideal design&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Everything above assumes you get to choose where processing runs. Sometimes you don't, and residency, encryption, and hardware outrank performance.&lt;/p&gt;

&lt;p&gt;MemoMeister was restricted to third-party services that stored and processed data within Germany. That single compliance line forced a self-hosted OnlyOffice deployment instead of a managed document service — an architectural decision made by a regulation, not a benchmark.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devico.io/success-stories/visbion" rel="noopener noreferrer"&gt;Visbion&lt;/a&gt; is the sharper example: their Image Cube compresses and encrypts DICOM 3.0 medical images on dedicated routing hardware inside mobile scanning trailers, serving NHS Breast Screening Services as the UK's largest installed base of dedicated imaging routing hardware. The heaviest processing happens at the edge, on a device in a trailer, before any cloud pipeline sees a byte — because bandwidth from a mobile trailer and encryption requirements for medical imaging say so. Our work there was consulting on fleet-deployment automation, compression and encryption refinement, and a modular redesign — tuning that constraint-driven architecture, not replacing it with a textbook one.&lt;/p&gt;

&lt;p&gt;The trade is real: edge and self-hosted processing give up elasticity and easy observability for compliance and bandwidth. Check these constraints before drawing the architecture, because they don't negotiate afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where to spend the effort, in order&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If I'm starting an ingestion system today, the order is: a cheap synchronous gate that rejects everything a user can fix on the spot; bytes going straight to object storage with an explicit commit; idempotent workers with a DLQ someone watches; and only then worker scaling and broker tuning. The per-format parsers will take longer than all of the infrastructure combined — budget accordingly.&lt;/p&gt;

&lt;p&gt;Reach for the full four-stage split when files are big, formats are hostile, and derived artifacts are what users actually consume. Skip the ceremony when you're thumbnailing avatars — a gate check and one background job is a complete architecture at that size.&lt;/p&gt;

&lt;p&gt;And check the read side last, every time. If the artifact coming out of the pipeline is still expensive to consume, no amount of queue work will save you — you've optimized the half of the system your users never touch.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>node</category>
      <category>aws</category>
    </item>
    <item>
      <title>How to Modernize a Legacy SaaS Without Freezing Feature Delivery: Refactor, Replatform, or Rewrite?</title>
      <dc:creator>Vic Tekys</dc:creator>
      <pubDate>Wed, 09 Sep 2026 14:30:07 +0000</pubDate>
      <link>https://dev.to/devico-solutions/how-to-modernize-a-legacy-saas-without-freezing-feature-delivery-refactor-replatform-or-rewrite-eeg</link>
      <guid>https://dev.to/devico-solutions/how-to-modernize-a-legacy-saas-without-freezing-feature-delivery-refactor-replatform-or-rewrite-eeg</guid>
      <description>&lt;p&gt;The moment arrives on every aging SaaS. Sales has commitments on the roadmap, and the codebase is telling you it can't carry them — every feature ships with a regression tax, the framework stopped getting patches years ago, and nobody fully understands the billing module anymore.&lt;/p&gt;

&lt;p&gt;The reflex answer is always the same: freeze the roadmap for two quarters, build "v2" on the side, migrate everyone over, resume shipping. I've sat in that meeting more than once.&lt;/p&gt;

&lt;p&gt;That reflex is wrong twice over. First, no revenue-generating SaaS actually gets a two-quarter freeze — customers keep filing tickets, competitors keep shipping, and the freeze quietly becomes four quarters. Second, it treats "modernize" as one decision you make once, for the whole system.&lt;/p&gt;

&lt;p&gt;It isn't. Refactor, replatform, or rewrite is a per-component verdict. And the way you avoid the freeze is to stop running modernization as a separate project and make it one backlog with feature delivery: build new capabilities as new services, refactor the code you're already touching, and rewrite only what genuinely can't be saved.&lt;/p&gt;

&lt;p&gt;The stakes of getting this wrong are documented. In a &lt;a href="https://vfunction.com/blog/survey-why-application-modernization-projects-fail/" rel="noopener noreferrer"&gt;2022 Wakefield Research survey&lt;/a&gt; of 250 senior IT leaders at large enterprises, 79% of app modernization projects failed, at an average cost of $1.5 million and a 16-month timeline. Those numbers describe big-bang programs at big companies — but the failure mode they capture is exactly the "pause everything, modernize, resume" shape most teams reach for by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The false trilemma: you don't choose once for the whole system&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Most refactor-vs-rewrite articles frame this as a single fork in the road. Pick a verb, apply it to the platform, done.&lt;/p&gt;

&lt;p&gt;Every real modernization I've been close to mixed the verbs — sometimes all three, over years.&lt;/p&gt;

&lt;p&gt;On &lt;a href="https://devico.io/success-stories/good-shape" rel="noopener noreferrer"&gt;GoodShape&lt;/a&gt;, a wellness platform that had been in production for about 15 years, we ran a microservices migration and heavy in-place refactoring at the same time: new capabilities went out as new services while hundreds of bugs got fixed and thousands of code smells got eliminated in the monolith that was still serving traffic.&lt;/p&gt;

&lt;p&gt;On &lt;a href="https://devico.io/success-stories/sunlayar" rel="noopener noreferrer"&gt;Sunlayar&lt;/a&gt;, the engagement started around the legacy QMPV solar-design platform — and only after working with it did the honest assessment land: continued maintenance would cost more than a rewrite, so a rewrite is what happened.&lt;/p&gt;

&lt;p&gt;Same company doing the work, opposite verdicts. Because the verdict belongs to the component and its situation, not to a philosophy.&lt;/p&gt;

&lt;p&gt;One warning before you adopt per-component thinking, though: it needs a whole-system owner. Somebody has to hold the target architecture in their head and decide which extraction happens next. Without that, "decide per component" degrades into a half-migrated estate that stays half-migrated forever — the worst of both worlds, permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Diagnose what's actually failing before picking a verb&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;"Legacy" is not a diagnosis. It's a bucket for at least four different diseases that get treated very differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An obsolete stack — frameworks and runtimes past end-of-life.&lt;/li&gt;
&lt;li&gt;Missing documentation — the system's behavior lives in people's heads, some of whom left.&lt;/li&gt;
&lt;li&gt;Inconsistent code — written by different developers across different eras, no common standards.&lt;/li&gt;
&lt;li&gt;An architecture at its scalability ceiling — the structure itself can't take more load or more change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first three are painful, but they're curable in place. Only the fourth justifies structural change — replatforming or rewriting.&lt;/p&gt;

&lt;p&gt;This distinction matters because tech debt pain is nearly universal and therefore nearly useless as a signal. In the &lt;a href="https://survey.stackoverflow.co/2024/professional-developers" rel="noopener noreferrer"&gt;2024 Stack Overflow survey&lt;/a&gt;, 62.4% of professional developers named technical debt as their top frustration — the number one answer among both ICs and managers. And McKinsey &lt;a href="https://www.mckinsey.com/capabilities/tech-and-ai/our-insights/tech-debt-reclaiming-tech-equity" rel="noopener noreferrer"&gt;estimated back in 2020&lt;/a&gt; that tech debt amounts to 20–40% of the value of a company's entire technology estate. If frustration were the trigger for a rewrite, everything would get rewritten. The trigger has to be sharper than that.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Architecture at its ceiling vs. code that's merely ugly&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The question I actually ask: &lt;strong&gt;can this architecture carry the next two years of roadmap?&lt;/strong&gt; Not "is this code nice." Not "would we build it this way today." Nobody would build any five-year-old system the way they'd build it today; that's not a finding.&lt;/p&gt;

&lt;p&gt;GoodShape had all four diseases at once — no documentation, no common coding standards, an obsolete stack, &lt;em&gt;and&lt;/em&gt; a monolith that had nearly reached peak performance, with further scaling becoming impossible. That last item is what earned structural change. The other three would have been refactor-shaped on their own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devico.io/success-stories/medneo" rel="noopener noreferrer"&gt;medneo&lt;/a&gt;, a radiology-as-a-service provider operating across Germany and the UK, looked superficially similar — a Radiology Information System degrading under a growing patient load, with regressions creeping in as features accreted. But the architecture could still carry the roadmap. The diseases were slow code, inefficient database access, and outdated frameworks. Refactor-shaped, all of it.&lt;/p&gt;

&lt;p&gt;One more bias to name: inside teams overwhelmingly want the rewrite. It's more fun, and it promises escape from code they resent. But fifteen years of production code encodes fifteen years of edge cases, regulatory quirks, and 3 a.m. fixes that nobody remembers making. Joel Spolsky called the full rewrite &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;"the single worst strategic mistake"&lt;/a&gt; back in 2000, and the reason he gave — old code has been &lt;em&gt;used&lt;/em&gt;, and using code shakes bugs out of it — hasn't aged a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Refactor in place — when the foundation still holds&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Refactoring is the least glamorous verdict and the one I end up defending most often.&lt;/p&gt;

&lt;p&gt;On medneo, the work was exactly as unglamorous as it sounds: optimize hot paths, eliminate unnecessary database calls, upgrade legacy frameworks and libraries to supported versions, and stand up a CI/CD pipeline. Eight engineers, from 2019 to 2022.&lt;/p&gt;

&lt;p&gt;The outcomes — significantly reduced average load times, fewer outages, and new rollouts that stopped degrading performance — are qualitative, because the client doesn't publish before/after numbers. I'd rather tell you that plainly than invent a percentage. But the strategic point is quantitative enough: three years of platform improvement happened &lt;em&gt;while the RIS kept serving hospitals&lt;/em&gt;, with zero freeze, because none of that work required stopping feature delivery. Refactoring almost never does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devico.io/success-stories/soapbox" rel="noopener noreferrer"&gt;Soapbox&lt;/a&gt;, an e-commerce fulfillment platform, ran the same play at a much earlier stage: a React Native warehouse app that had shipped as a prototype — poorly structured code, plenty of bugs, no separation between dev, staging, and production. The team did a global refactoring of the core logic, put a linter in place, set up GitLab and CodePush, and split the environments — and shipped more than 15 releases along the way, including entirely new features like stocktaking, warehouse audit, and barcode scanning. Cleanup and delivery, one backlog, four people.&lt;/p&gt;

&lt;p&gt;The limitation is the one from the diagnosis section, and it's hard: refactoring cannot raise an architectural ceiling. If the structure itself is the disease, no amount of in-place cleanup cures it. You'll just have beautiful code that still can't scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Replatform incrementally — migration under live traffic&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The middle verdict is the one that actually answers this article's title, so it gets the most detail.&lt;/p&gt;

&lt;p&gt;GoodShape is the fullest version of it I've worked near. The platform had been in production about 15 years, serving 200+ UK employers — including NHS organizations, Philips, and Heathrow Express — with 24/7 nurse support riding on it. You do not take that offline for a rebuild. The verdict on the monolith was replatform: migrate toward microservices, incrementally, under live traffic. A team of 7, working since 2018, on Java 17 and Spring Boot with React/Redux up front.&lt;/p&gt;

&lt;p&gt;The mechanism is what matters, because it's the anti-freeze tactic in concrete form:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New capabilities ship as new services.&lt;/strong&gt; Push notifications and fitness-tracker data collection — integrating 16 different tracker brands — were built as standalone microservices from day one. The roadmap item &lt;em&gt;was&lt;/em&gt; the migration step. Nobody had to choose between them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code you're already touching gets refactored.&lt;/strong&gt; A few hundred bugs fixed and thousands of code smells eliminated — not as a dedicated cleanup phase, but woven through feature work in the monolith.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation gets written as you go&lt;/strong&gt;, capability by capability, instead of as a doomed up-front archaeology project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The frontend got split into subprojects&lt;/strong&gt; and refactored with updated React components — same incremental logic, applied to the UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even purely manual pain got folded in: monthly client reporting had been a hand-built 50-slide PowerPoint; it became an automated reporting feature. Roadmap value and modernization, again the same line item.&lt;/p&gt;

&lt;p&gt;If you want the underlying pattern with a name, it's Martin Fowler's &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;strangler fig&lt;/a&gt;: new growth wraps the old system until the old system is no longer load-bearing. Stripped to its skeleton, the runtime side is just routing — carve one capability's traffic away from the monolith at the edge:&lt;/p&gt;

&lt;h1&gt;
  
  
  Everything still goes to the monolith...
&lt;/h1&gt;

&lt;p&gt;location / {&lt;/p&gt;

&lt;p&gt;proxy_pass &lt;a href="http://legacy-monolith" rel="noopener noreferrer"&gt;http://legacy-monolith&lt;/a&gt;;&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;h1&gt;
  
  
  ...except the capabilities that have been extracted.
&lt;/h1&gt;

&lt;p&gt;location /api/notifications/ {&lt;/p&gt;

&lt;p&gt;proxy_pass &lt;a href="http://notification-service" rel="noopener noreferrer"&gt;http://notification-service&lt;/a&gt;;&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;location /api/tracker-data/ {&lt;/p&gt;

&lt;p&gt;proxy_pass &lt;a href="http://tracker-service" rel="noopener noreferrer"&gt;http://tracker-service&lt;/a&gt;;&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The routing is the easy part. The discipline is the hard part, and it's the limitation to be honest about: this is a multi-year &lt;em&gt;posture&lt;/em&gt;, not a project with an end date — GoodShape's has been running since 2018. You need that whole-system owner deciding which extractions the roadmap actually justifies, or you'll extract whatever's fun instead of whatever's next.&lt;/p&gt;

&lt;p&gt;One more honesty note, since it's tempting to oversell: GoodShape won gold at the 2020 ECCCSA awards for application of technology while all this was mid-flight. I'm not claiming the migration caused that — the sources don't, so I won't. The claim I &lt;em&gt;am&lt;/em&gt; making is narrower and more useful: the business never had to stop operating, competing, or shipping while its 15-year-old core was being restructured underneath it. And the scalability gains came from the migration &lt;em&gt;plus*the refactoring *plus&lt;/em&gt; the process changes — not from microservices as a magic word.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Rewrite — the expensive verdict, and what earns it&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Sometimes the honest answer is that the patient doesn't recover in place.&lt;/p&gt;

&lt;p&gt;The threshold I use: rewrite when maintaining the old platform costs more than replacing it, &lt;em&gt;and&lt;/em&gt; little of it is worth preserving. Both conditions. The first alone describes half the software in production; it's the second that earns the verdict.&lt;/p&gt;

&lt;p&gt;Sunlayar met both. The QMPV platform for planning rooftop solar projects ran on obsolete technologies, and as complexity grew it became impossible to add new business functions quickly — the assessment concluded a rewrite would beat continued maintenance. So it was rebuilt as a new platform on Node.js, TypeScript, React, and MongoDB, by a team of 2 developers and 2 QA engineers, in an engagement running since 2014. The rewritten platform outlived its original owner: QMPV was later acquired by Esdec, and the platform is still in production today as EcoFasten's Design Assistant Project Calculator. (Again — the acquisition is a fact of the timeline, not a result I'm attributing to the rewrite.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devico.io/success-stories/bego" rel="noopener noreferrer"&gt;BEGO&lt;/a&gt;, a German dental company, is the other flavor: not one obsolete platform but several independent portals, with an architecture where every new feature had to work around structural limitations. The verdict was rebuild-as-one-platform — React, TypeScript, NestJS, PostgreSQL, Kubernetes — delivered by a team of about six, with 14 major features, cooperation running since 2025, and a high daily order volume from launch.&lt;/p&gt;

&lt;p&gt;But here's the detail that matters most for this article: even BEGO wasn't a pure greenfield rewrite. The proven backend file-processing components were carefully reused. "Rewrite" as a verdict doesn't mean "salvage nothing" — it means salvage &lt;em&gt;selectively&lt;/em&gt;, keeping the parts that have earned their production scars.&lt;/p&gt;

&lt;p&gt;The limitation: rewrites only look fast in retrospect. Sunlayar's engagement spans over a decade; nothing about these timelines is quick. If someone pitches you a rewrite with a confident six-month schedule, that schedule is the first bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The process layer that keeps features shipping&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;None of the above works without an unglamorous substrate, and it shows up in every one of these cases.&lt;/p&gt;

&lt;p&gt;Environment separation, so modernization work and feature work can land in the same codebase without stepping on each other — Soapbox literally could not do both safely until dev, staging, and prod were split. CI/CD, so the cost of shipping small stays near zero — medneo's pipeline is what let optimization work roll out continuously instead of batching into risky releases. Automated linting, so a refactored module and a legacy module stop diverging in style the day after cleanup. Documentation as a habit, which is what GoodShape used to stop the no-docs disease from re-infecting every new service.&lt;/p&gt;

&lt;p&gt;The trap: process work reads as slowdown first. It's the line item that gets cut when the roadmap pressure is highest — which is exactly the moment you need it, because it's the only thing that lets one team serve two masters in one codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The question to write down first&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The three verdicts aren't symmetric, and choosing between them gets easier once you say the asymmetry out loud.&lt;/p&gt;

&lt;p&gt;Refactoring is cheap and reversible — if it turns out to be insufficient, you've lost little and the code is better for whatever comes next. Incremental replatforming trades speed for continuity — slower than the big-bang fantasy, but the platform never stops earning. Rewriting is a one-way door — occasionally still the correct door, as Sunlayar and BEGO show, but you don't walk through it because the old code is annoying. You walk through it when maintenance costs more than replacement and almost nothing is worth saving — and even then, you carry the proven parts with you.&lt;/p&gt;

&lt;p&gt;So before picking a verb, write down the answer to one question, per component: &lt;strong&gt;can this architecture carry the next two years of roadmap?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where yes — refactor what you touch, and keep shipping. Where no, but the capability boundaries are extractable — replatform incrementally, and let the roadmap decide the extraction order. Where no, and nothing's worth saving — rewrite, salvage selectively, and be honest with everyone about the years, not months, you just signed up for.&lt;/p&gt;

&lt;p&gt;The one answer that's never right is the one that sounds safest in the meeting: freeze everything and build v2 on the side. That's not a modernization strategy. That's the 79%.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>softwareengineering</category>
      <category>programming</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Keep a Data-Heavy React App Fast</title>
      <dc:creator>Vic Tekys</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:18:24 +0000</pubDate>
      <link>https://dev.to/devico-solutions/how-to-keep-a-data-heavy-react-app-fast-n1</link>
      <guid>https://dev.to/devico-solutions/how-to-keep-a-data-heavy-react-app-fast-n1</guid>
      <description>&lt;p&gt;A React dashboard that feels perfectly responsive with 200 rows may become almost unusable at 20,000. Memoization is often the first proposed fix, but React.memo cannot help much when the browser is laying out thousands of DOM nodes or a large data transformation is blocking the main thread.&lt;/p&gt;

&lt;p&gt;Those problems may produce the same visible symptom—a slow interface—but they have different causes. Rendering too much calls for virtualization. Expensive JavaScript may belong in a Web Worker. Oversized or repeatedly downloaded datasets point toward pagination, caching, or IndexedDB. Media introduces another set of costs.&lt;/p&gt;

&lt;p&gt;Getting the diagnosis right matters because users experience far more than the initial page load. Google estimates that roughly &lt;a href="https://web.dev/articles/inp" rel="noopener noreferrer"&gt;90%&lt;/a&gt; of a user's time on a page is spent after it has loaded, when responsiveness depends on how quickly the application handles input, updates data, and paints the next frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the bottleneck before choosing the optimization
&lt;/h2&gt;

&lt;p&gt;Most performance problems in data-heavy React applications fall into four categories.&lt;/p&gt;

&lt;p&gt;Rendering cost grows with the number and complexity of DOM nodes React creates, reconciles, and updates. A table containing 10,000 visible rows is expensive even if every component is neatly organized.&lt;/p&gt;

&lt;p&gt;Computation cost comes from JavaScript occupying the main thread. Sorting large arrays, parsing CSV files, filtering records, aggregating chart data, and transforming API responses can all delay keyboard input, scrolling, and rendering.&lt;/p&gt;

&lt;p&gt;Network cost usually appears as an oversized response or a chain of dependent requests. A 4 MB JSON payload is one kind of problem; twelve sequential API calls are another.&lt;/p&gt;

&lt;p&gt;Media cost becomes significant when an application loads large images, videos, or 3D assets before they are needed—or serves them at a higher resolution than the interface can display.&lt;/p&gt;

&lt;p&gt;Memoization has a narrower role than it is often given. React.memo and useCallback can prevent avoidable component updates, while useMemo can prevent the same calculation from being repeated when its dependencies have not changed. None of them makes an unavoidable 400 ms transformation cheap, reduces a large API response, or removes thousands of DOM nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the React Profiler
&lt;/h2&gt;

&lt;p&gt;Record the interaction that feels slow: changing a filter, sorting a table, opening a tab, or selecting a chart range.&lt;/p&gt;

&lt;p&gt;The React DevTools Profiler shows which components rendered, how long the commit took, and what triggered the update. A wide flame graph containing hundreds of component renders usually points to a rendering problem. Depending on what the trace shows, the answer may be virtualization, more stable props, narrower state subscriptions, or selective memoization.&lt;/p&gt;

&lt;p&gt;The same profiling model applies in React 18 and 19, so upgrading React does not change the basic diagnostic workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check what is blocking the main thread
&lt;/h2&gt;

&lt;p&gt;Record the same interaction in the Chrome DevTools Performance panel. Look for long tasks—work that occupies the main thread for &lt;a href="https://developer.chrome.com/docs/web-platform/long-animation-frames" rel="noopener noreferrer"&gt;50 ms or more&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A long JavaScript block during a calculation, without a corresponding burst of React rendering, suggests a computation bottleneck. Virtualizing a list will not fix it. The calculation may need to be reduced, scheduled differently, or moved to a Web Worker.&lt;/p&gt;

&lt;p&gt;Chrome's &lt;a href="https://developer.chrome.com/blog/loaf-has-shipped" rel="noopener noreferrer"&gt;Long Animation Frames API&lt;/a&gt;, available since Chrome 123, adds more detail by attributing slow frames to the scripts responsible for them. That makes it useful when investigating poor Interaction to Next Paint results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the request waterfall
&lt;/h2&gt;

&lt;p&gt;The Network panel answers two different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much data is being transferred?&lt;/li&gt;
&lt;li&gt;Are requests happening in parallel or waiting unnecessarily for one another?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single 3 MB response calls for pagination, compression, or a smaller field selection. A diagonal sequence of independent requests suggests a waterfall that could be flattened. Client-side caching may improve repeat navigation, but it does not make the first oversized response smaller.&lt;/p&gt;

&lt;p&gt;The central distinction is simple: are there too many elements to render, or is one piece of JavaScript preventing the browser from rendering at all? The React Profiler is best suited to the first question; the Performance panel helps answer the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match the symptom to the fix
&lt;/h2&gt;

&lt;p&gt;The techniques in this article are complementary, but they are not interchangeable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the page janks while scrolling through a long table or list, the likely cause is rendering overload created by thousands of DOM nodes. Start with list or table virtualization using react-window, TanStack Virtual, or React Virtuoso. Keep in mind that off-screen content will not be present in the DOM, so accessibility, find-in-page, and SEO may require additional work.&lt;/li&gt;
&lt;li&gt;If the interface freezes during sorting, filtering, parsing, or aggregation, heavy computation is probably blocking the main thread. Try reducing the amount of work or moving the calculation to a Web Worker. Workers add serialization overhead, cannot access the DOM, and require additional coordination code.&lt;/li&gt;
&lt;li&gt;If a large dataset is repeatedly downloaded or kept in memory, consider persisting suitable data in IndexedDB with Dexie.js, idb, or RxDB. This reduces repeated loading and memory pressure, but the application must handle browser eviction, synchronization, schema changes, and stale data.&lt;/li&gt;
&lt;li&gt;If the same requests run repeatedly or form a network waterfall, use TanStack Query or SWR together with data prefetching and parallel requests. The trade-off is that caching introduces questions around invalidation, staleness, and memory consumption.&lt;/li&gt;
&lt;li&gt;If images, videos, or 3D assets delay interaction, start with compression, right-sizing, lazy loading, and intent-based preloading. Placeholders require additional UX work, while excessive prefetching can waste bandwidth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A complex application may need several of these changes. A large 3D product viewer, for instance, might use a Worker for calculations, IndexedDB for model data, virtualization for property lists, and preloading for media. The important part is that each technique addresses a measured cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Virtualization, pagination, or both?
&lt;/h2&gt;

&lt;p&gt;Virtualization and pagination both limit how much work the browser performs, but they change the product in different ways.&lt;/p&gt;

&lt;p&gt;Virtualization maintains the experience of one continuous dataset. It renders only the rows inside or near the viewport and reuses DOM nodes as the user scrolls. A table may contain 50,000 records while keeping only a few dozen row elements mounted.&lt;/p&gt;

&lt;p&gt;That makes virtualization a natural fit for authenticated dashboards, chat histories, log viewers, data grids, and infinite feeds. The compromise is that off-screen items do not exist in the DOM. Browser find-in-page cannot discover them, crawlers cannot index them, and some assistive-technology workflows require deliberate focus and navigation handling.&lt;/p&gt;

&lt;p&gt;Pagination places a fixed number of items on each page and provides controls for moving between pages. It is usually easier to make crawlable, linkable, and accessible. Public search results, product categories, article indexes, and other SEO-relevant listings generally benefit from this model.&lt;/p&gt;

&lt;p&gt;A hybrid approach is often the practical answer. The server returns a bounded page of records, and the client virtualizes that page if it is still large. Payload sizes remain controlled, while the browser avoids mounting more rows than the user can see.&lt;/p&gt;

&lt;p&gt;Remember that grids can require two forms of virtualization. Row virtualization helps when there are thousands of records. Column virtualization matters when a financial or analytical grid contains dozens of columns. A library that handles simple lists well may not be equally capable with two-dimensional grids, sticky columns, variable row heights, and keyboard navigation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Virtualization lets the user scroll through one continuous list while keeping only the visible rows in the DOM. It works well for dashboards, feeds, logs, chat histories, and authenticated applications. The downside is that off-screen content may not be indexed by search engines, while accessibility and browser find-in-page require additional work.&lt;/li&gt;
&lt;li&gt;Pagination divides results into separate pages and keeps the number of DOM elements under control. Pages can be indexed, linked, and shared individually. It is usually the better choice for public listings, product directories, article indexes, and search results.&lt;/li&gt;
&lt;li&gt;Server pagination combined with client-side virtualization provides a middle ground. The server returns a limited page of data, while the client renders only the visible part of that page. This works well for large authenticated datasets and complex data grids, although it requires managing both pagination and virtualization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For libraries, react-window remains a focused option for straightforward lists and is &lt;a href="https://www.npmjs.com/package/react-window" rel="noopener noreferrer"&gt;actively maintained&lt;/a&gt;. Its older sibling, &lt;a href="https://www.npmjs.com/package/react-virtualized" rel="noopener noreferrer"&gt;react-virtualized&lt;/a&gt;, is rarely the first choice for a new implementation.&lt;/p&gt;

&lt;p&gt;TanStack Virtual is headless, which suits teams that want complete control over markup and styling or already use the TanStack ecosystem. React Virtuoso provides more behavior out of the box and is particularly useful for variable-height content.&lt;/p&gt;

&lt;p&gt;The library decision should follow the interface requirements. A basic fixed-height list does not need the same machinery as an editable grid with pinned columns, dynamic heights, grouped rows, and accessible keyboard navigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move expensive calculations to Web Workers
&lt;/h2&gt;

&lt;p&gt;When a trace shows a long calculation without a corresponding rendering spike, moving that work off the main thread can restore responsiveness. A Web Worker can sort, parse, aggregate, or transform data while the browser continues to process input and paint frames.&lt;/p&gt;

&lt;p&gt;Workers have firm boundaries. They cannot access the DOM, window, or localStorage. They can use fetch, WebSockets, and IndexedDB, which means a worker can retrieve, transform, and persist a large dataset without routing every step through the UI thread.&lt;/p&gt;

&lt;p&gt;Messages between the main thread and a worker normally use the structured-clone algorithm. Large values must be copied, and that serialization cost can erase part of the expected performance gain.&lt;br&gt;
Transferable objects avoid that copy for resources such as ArrayBuffer. Ownership moves from one thread to the other, leaving the original buffer unusable on the sending side. For binary files, large typed arrays, and some data-processing pipelines, this can be considerably cheaper than cloning.&lt;/p&gt;

&lt;p&gt;SharedArrayBuffer allows genuine shared memory, but it introduces stricter deployment requirements. The page must be &lt;a href="https://web.dev/articles/coop-coep" rel="noopener noreferrer"&gt;cross-origin isolated&lt;/a&gt; using COOP and COEP headers, which may conflict with embedded third-party content. Most applications can use transferable objects without taking on that constraint.&lt;/p&gt;

&lt;p&gt;The native postMessage interface is manageable for one or two worker operations but becomes tedious as the API grows. &lt;a href="https://github.com/GoogleChromeLabs/comlink" rel="noopener noreferrer"&gt;Comlink&lt;/a&gt;, a small library from Google Chrome Labs, wraps worker communication in a proxy so asynchronous worker functions can be called more like ordinary methods.&lt;/p&gt;

&lt;p&gt;A worker pool can help when the application runs many independent jobs. Instead of creating a worker for every task, the application maintains a small number of workers and feeds them work from a queue. The pool size still needs restraint: saturating every available core may speed up the calculation while competing with the browser for resources.&lt;/p&gt;

&lt;p&gt;Workers do not reduce layout or painting cost. A page that mounts 10,000 rows will remain expensive after its data transformation moves off-thread. Workers address computation; virtualization addresses rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use IndexedDB when the browser needs a real local data store
&lt;/h2&gt;

&lt;p&gt;React state is not persistent storage, and localStorage is poorly suited to large datasets. It stores strings, operates synchronously, and commonly offers only a few megabytes per origin. Reading or parsing a large value can block the main thread before the application renders anything useful.&lt;/p&gt;

&lt;p&gt;IndexedDB is asynchronous and transactional, with a practical capacity far beyond localStorage, depending on the browser, device, and available disk space. It can store objects, typed arrays, Blob values, and files without manually converting everything to JSON.&lt;/p&gt;

&lt;p&gt;That makes it useful for offline-first and local-first products, large working sets that must survive a reload, and expensive data that can be reconstructed but should not be fetched on every visit.&lt;/p&gt;

&lt;p&gt;The native IndexedDB API is verbose and event-driven, so most React applications use a wrapper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dexie.js provides a promise-based database and query layer.&lt;/li&gt;
&lt;li&gt;idb stays close to the native API while replacing the event-based interface with promises.&lt;/li&gt;
&lt;li&gt;RxDB adds reactive queries and replication for applications that need live synchronization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choosing IndexedDB changes more than storage. Once a working copy lives in the browser, the application needs rules for refreshing it, resolving conflicts, migrating schemas, and distinguishing fresh data from stale data. A local-first architecture is a data-model decision, not a larger version of a cache.&lt;/p&gt;

&lt;p&gt;Eviction also has to be expected. Browsers may remove storage under pressure. Safari applies a particularly strict tracking-prevention policy under which script-writable storage, including IndexedDB, can be deleted after seven days without user interaction with the site.&lt;/p&gt;

&lt;p&gt;Unless the product explicitly guarantees durable browser storage, treat IndexedDB as a local copy that can be rebuilt. The server—or another controlled persistence layer—should remain the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache repeat work and remove network waterfalls
&lt;/h2&gt;

&lt;p&gt;After rendering and computation have been addressed, repeated network work often becomes the next visible delay.&lt;/p&gt;

&lt;p&gt;TanStack Query and SWR solve several problems that otherwise end up scattered through components: request deduplication, cached results, background refreshes, retry behavior, and stale-data handling. They can return an existing result immediately during repeat navigation instead of showing another loading state for data the user has already seen.&lt;/p&gt;

&lt;p&gt;TanStack Query's defaults deserve attention. It considers fetched data &lt;a href="https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults" rel="noopener noreferrer"&gt;stale immediately&lt;/a&gt; by default, while unused cache entries are garbage-collected after five minutes. Those settings are safe but can produce unnecessary refetches when the underlying data changes infrequently. staleTime should reflect how quickly each resource can become outdated, not one global guess for the entire application.&lt;/p&gt;

&lt;p&gt;Both libraries support the stale-while-revalidate pattern: return cached data first, verify it in the background, and update the interface if the response has changed. It improves perceived performance because repeat visits do not begin with an empty screen.&lt;br&gt;
Prefetching can remove the remaining wait from predictable navigation. Route data can be requested when the user hovers over a link, focuses a control, or reaches a point where the next action is likely. Prefetch too aggressively, however, and the application downloads data the user never requests.&lt;/p&gt;

&lt;p&gt;Request structure matters just as much as caching. Independent calls should begin together through Promise.all, parallel queries, or a backend endpoint designed to aggregate them. A five-request waterfall cannot be corrected by memoizing the component that waits for it.&lt;br&gt;
Media benefits from the same discipline. A gallery, 3D viewer, or image-heavy customer interface can preload the next likely asset, retain previously fetched media, and lazy-load everything outside the active view. Service-worker caching through Workbox can extend the approach to unreliable connections and offline use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure whether the change helped
&lt;/h2&gt;

&lt;p&gt;A performance change is only useful if it improves the interaction users actually perform.&lt;/p&gt;

&lt;p&gt;Interaction to Next Paint (INP) replaced First Input Delay as a Core Web Vital in March 2024. Unlike FID, which evaluated only the first interaction, INP considers interactions across the visit. A result of 200 ms or less at the 75th percentile is considered good.&lt;/p&gt;

&lt;p&gt;That makes INP relevant to data-heavy React products, where roughly 90% of a user's time may occur after the initial load. A dashboard can achieve an excellent Largest Contentful Paint score and still feel slow every time someone filters a table.&lt;/p&gt;

&lt;p&gt;Core Web Vitals will not capture every product-specific operation. “Time to render 10,000 rows,” “time to display the first interactive chart,” and “duration of a CSV import” require custom instrumentation.&lt;/p&gt;

&lt;p&gt;The Performance API provides the necessary primitives:&lt;br&gt;
performance.mark("filter-start");&lt;/p&gt;

&lt;p&gt;// Run the operation.&lt;/p&gt;

&lt;p&gt;performance.mark("filter-end");&lt;br&gt;
performance.measure("filter-duration", "filter-start", "filter-end");&lt;/p&gt;

&lt;p&gt;Benchmark before and after under the same conditions: identical dataset, device profile, browser version, and network throttling. Comparing a developer laptop with a production user's older machine produces an impressive number, but not a useful one.&lt;/p&gt;

&lt;p&gt;Lab and field measurements answer different questions. Lighthouse and local traces provide controlled, repeatable feedback while engineers iterate. Real-user monitoring, CrUX, and the web-vitals library show what happens across the hardware and network conditions users actually have. Lab data helps isolate the change; field data confirms that the improvement survived production.&lt;/p&gt;

&lt;h2&gt;
  
  
  How these techniques combine in production
&lt;/h2&gt;

&lt;p&gt;The strongest implementations rarely depend on one optimization.&lt;/p&gt;

&lt;p&gt;Vitus, a construction-intelligence platform that works with several large 3D models, combined Web Workers, list virtualization, and IndexedDB. Model calculations moved off the main thread. React Virtuoso handled long property and data lists because pagination did not fit the product requirements. Dexie.js stored large model data locally instead of keeping the entire working set in React state.&lt;/p&gt;

&lt;p&gt;A separate change—from the previous model format to SVF2 with parallel loading—cut model size roughly &lt;a href="https://devico.io/success-stories/vitus" rel="noopener noreferrer"&gt;5× and load time about 2.5×&lt;/a&gt;. That result came from addressing several layers: computation, rendering, storage, and network transfer.&lt;/p&gt;

&lt;p&gt;Datasport, a sports-event platform with chart- and table-heavy dashboards, needed a different combination. Large tables were &lt;a href="https://devico.io/success-stories/datasport" rel="noopener noreferrer"&gt;virtualized&lt;/a&gt; because pagination was not suitable, GET requests were cached, and component memoization reduced unnecessary rendering. React Context replaced Redux in areas where a smaller state layer was sufficient. Web Workers were not part of the solution because main-thread computation was not the defining bottleneck.&lt;/p&gt;

&lt;p&gt;The Sales Platform, a media-heavy fashion-tech interface, paired virtualization with a dedicated media layer. Virtualized rendering reduced the number of mounted elements by about &lt;a href="https://devico.io/success-stories/sales-platform-for-seamless-customer-interactions" rel="noopener noreferrer"&gt;70%&lt;/a&gt;, while preloading and caching reduced wait times by 50%. SWR managed data caching, and Workbox supported service-worker caching and offline behavior.&lt;/p&gt;

&lt;p&gt;The same constraints appear in analytics products. &lt;a href="https://devico.io/success-stories/gaze-health" rel="noopener noreferrer"&gt;GazeHealth&lt;/a&gt;, a mental-health analytics platform, renders a custom D3 Sankey diagram alongside roughly fifteen data visualizations over large datasets. &lt;a href="https://devico.io/success-stories/good-shape" rel="noopener noreferrer"&gt;GoodShape&lt;/a&gt;, an employee-wellness platform, combines complex charts with AG Grid tables and optimized large-data processing.&lt;/p&gt;

&lt;p&gt;The projects use related tools, but not identical stacks. That is the useful lesson: performance work begins with the bottleneck, not with a preferred library.&lt;/p&gt;

&lt;h2&gt;
  
  
  When specialized performance engineering makes sense
&lt;/h2&gt;

&lt;p&gt;One slow table or blocking filter is usually a bounded problem. An experienced engineer can profile the interaction, identify the expensive work, and implement a focused fix without redesigning the application.&lt;/p&gt;

&lt;p&gt;The decision changes when performance problems cross architectural boundaries. Worker APIs, client-side databases, synchronization rules, data-fetching layers, and two-dimensional virtualization all create decisions that other parts of the product will depend on. Those changes need ongoing ownership.&lt;/p&gt;

&lt;p&gt;Capacity matters as well. A team may understand the problem but lack time to investigate it without delaying product work. Engineers who have already implemented virtualized grids, worker pipelines, and IndexedDB-backed data layers can often recognize the performance signature faster than a team encountering it for the first time.&lt;/p&gt;

&lt;p&gt;External help is most useful when the scope is explicit. A short engagement can address a measured bottleneck. A dedicated team is more appropriate when performance work affects the application's data architecture and will continue across multiple releases.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webperf</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
