<?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: vanced youtube</title>
    <description>The latest articles on DEV Community by vanced youtube (@realvancedyoutube).</description>
    <link>https://dev.to/realvancedyoutube</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%2Fuser%2Fprofile_image%2F3866426%2Fa32ea547-a462-4a33-9ca2-c7345f646808.png</url>
      <title>DEV Community: vanced youtube</title>
      <link>https://dev.to/realvancedyoutube</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/realvancedyoutube"/>
    <language>en</language>
    <item>
      <title>Background Tasks in Kotlin Multiplatform: Unifying Android WorkManager and iOS BGTaskScheduler</title>
      <dc:creator>vanced youtube</dc:creator>
      <pubDate>Wed, 02 Sep 2026 15:06:49 +0000</pubDate>
      <link>https://dev.to/realvancedyoutube/background-tasks-in-kotlin-multiplatform-unifying-android-workmanager-and-ios-bgtaskscheduler-3no4</link>
      <guid>https://dev.to/realvancedyoutube/background-tasks-in-kotlin-multiplatform-unifying-android-workmanager-and-ios-bgtaskscheduler-3no4</guid>
      <description>&lt;p&gt;Writing shared Kotlin code across Android and iOS is genuinely productive — until you need to schedule work that runs in the background. At that point the platforms diverge sharply, and the usual KMP approach of "write once, adapt per platform" gets uncomfortable fast.&lt;/p&gt;

&lt;p&gt;On Android you have &lt;strong&gt;Jetpack WorkManager&lt;/strong&gt;, a robust, battle-tested API backed by the OS job scheduler. It handles constraints (network, charging, idle), exponential backoff, unique work policies, and periodic tasks. On iOS you have &lt;strong&gt;BGTaskScheduler&lt;/strong&gt;, Apple's tightly controlled background execution framework, which gives you roughly 30 seconds for a refresh task and zero guarantees about &lt;em&gt;when&lt;/em&gt; it will actually run.&lt;/p&gt;

&lt;p&gt;These two systems are not just different APIs — they represent genuinely different philosophies about how much control an app should have over when its code runs. Getting a single Kotlin interface to sit cleanly over both, without leaking platform assumptions into shared code, requires some deliberate design choices.&lt;/p&gt;

&lt;p&gt;This is a walkthrough of &lt;a href="https://github.com/neuralheads/kmpworker" rel="noopener noreferrer"&gt;KMPWorker&lt;/a&gt;, an open-source library we built at NeuralHeads to solve exactly this problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Interface Problem
&lt;/h2&gt;

&lt;p&gt;Before writing any Android or iOS code, the first decision was: what does a platform-agnostic task API actually look like?&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;KmpWorker&lt;/code&gt; interface in &lt;code&gt;core&lt;/code&gt; is the answer. It defines the contract that both &lt;code&gt;AndroidKmpWorker&lt;/code&gt; and &lt;code&gt;IOSKmpWorker&lt;/code&gt; implement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;KmpWorker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TaskRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;registerWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="nc"&gt;TaskExecutionContext&lt;/span&gt;&lt;span class="p"&gt;.()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;enqueueChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TaskChain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChainPolicy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;register&lt;/code&gt; / &lt;code&gt;enqueue&lt;/code&gt; split is intentional. Handlers are registered at app startup (before enqueue ever gets called), and enqueue schedules the actual work. On iOS this matters because &lt;code&gt;BGTaskScheduler&lt;/code&gt; requires all identifiers to be registered with the OS &lt;em&gt;before&lt;/em&gt; &lt;code&gt;applicationDidFinishLaunching&lt;/code&gt; returns — you cannot register a task handler lazily at the point you want to run it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TaskState&lt;/code&gt; flows through Kotlin's &lt;code&gt;Flow&amp;lt;TaskState&amp;gt;&lt;/code&gt;, covering the full lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scheduled → Running → Success
                    → Failed(throwable, retryCount, willRetry)
                    → Cancelled(reason)
                    → TimedOut(afterMillis)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this is a shared &lt;code&gt;Flow&lt;/code&gt;, UI code in either Android or iOS (via Swift interop) can observe state changes reactively, without polling or callbacks.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Android Maps to WorkManager
&lt;/h2&gt;

&lt;p&gt;The Android implementation lives in &lt;code&gt;AndroidTaskScheduler&lt;/code&gt;. For each &lt;code&gt;TaskRequest&lt;/code&gt;, it constructs a &lt;code&gt;OneTimeWorkRequest&lt;/code&gt; or &lt;code&gt;PeriodicWorkRequest&lt;/code&gt; and delegates to &lt;code&gt;WorkManager&lt;/code&gt;. The mapping is fairly direct for most cases.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TaskType&lt;/code&gt; is a sealed class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;OneTime&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TaskType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Periodic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;repeatIntervalMillis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TaskType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;ExactTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;runAtMillis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TaskType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Windowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;earliestMillis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;latestMillis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TaskType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ExactTime&lt;/code&gt; maps to WorkManager's &lt;code&gt;setInitialDelay()&lt;/code&gt;. This is worth calling out explicitly: WorkManager does not offer hard exact scheduling. The actual execution happens at or after the specified time, subject to battery optimizations and Doze mode. If your use case genuinely requires millisecond-precise execution, WorkManager is the wrong tool on Android regardless of any abstraction layer on top.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Windowed&lt;/code&gt; behaves similarly — the &lt;code&gt;earliestMillis&lt;/code&gt; becomes the initial delay and the &lt;code&gt;latestMillis&lt;/code&gt; is informational in the current Android implementation (WorkManager has a flex interval for &lt;code&gt;PeriodicWorkRequest&lt;/code&gt;, but not directly for one-time tasks).&lt;/p&gt;

&lt;p&gt;Constraints map cleanly to WorkManager's &lt;code&gt;Constraints.Builder&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;buildWorkConstraints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kmpConstraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Constraints&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;androidx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Constraints&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Constraints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequiredNetworkType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;kmpConstraints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requiresUnmeteredNetwork&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;NetworkType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UNMETERED&lt;/span&gt;
                &lt;span class="n"&gt;kmpConstraints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requiresNonRoamingNetwork&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;NetworkType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NOT_ROAMING&lt;/span&gt;
                &lt;span class="n"&gt;kmpConstraints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requiresInternet&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;NetworkType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CONNECTED&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;NetworkType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NOT_REQUIRED&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequiresCharging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kmpConstraints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requiresCharging&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequiresBatteryNotLow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kmpConstraints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batteryNotLow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequiresDeviceIdle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kmpConstraints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requiresDeviceIdle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Network constraint resolution follows a priority order: &lt;code&gt;requiresUnmeteredNetwork&lt;/code&gt; takes precedence over &lt;code&gt;requiresNonRoamingNetwork&lt;/code&gt;, which takes precedence over &lt;code&gt;requiresInternet&lt;/code&gt;. Only one &lt;code&gt;NetworkType&lt;/code&gt; can be set in WorkManager, so the library resolves the most restrictive constraint.&lt;/p&gt;

&lt;p&gt;The actual work runs inside &lt;code&gt;KmpTaskWorker&lt;/code&gt;, which extends &lt;code&gt;CoroutineWorker&lt;/code&gt;. This is where retry logic, timeout enforcement, and telemetry bridging happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;doWork&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;taskId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KEY_TASK_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;TaskMonitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;TaskRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;TaskRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;TaskMonitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeoutCancellationException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;TaskMonitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimedOut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;afterMillis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;willRetry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RetryEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shouldRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryPolicy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;TaskMonitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;willRetry&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;willRetry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail worth noting: the retry policy is serialized into WorkManager's &lt;code&gt;inputData&lt;/code&gt; as string constants, because WorkManager's &lt;code&gt;Data&lt;/code&gt; object only supports primitive types. The policy type, delay, and max retry count are stored as separate keys and reconstructed inside &lt;code&gt;KmpTaskWorker.readRetryPolicy()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The iOS Side: What BGTaskScheduler Actually Constrains
&lt;/h2&gt;

&lt;p&gt;iOS is harder. The &lt;code&gt;IOSTaskScheduler&lt;/code&gt; uses &lt;code&gt;BGTaskScheduler&lt;/code&gt; with two task types: &lt;code&gt;BGAppRefreshTask&lt;/code&gt; for &lt;code&gt;TaskType.OneTime&lt;/code&gt;, and &lt;code&gt;BGProcessingTask&lt;/code&gt; for &lt;code&gt;TaskType.Periodic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The most important thing to understand about &lt;code&gt;BGTaskScheduler&lt;/code&gt; — documented clearly in the repo's &lt;code&gt;docs/ios-limitations.md&lt;/code&gt; — is that the &lt;em&gt;entire scheduling decision belongs to Apple&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What your app controls&lt;/th&gt;
&lt;th&gt;What Apple controls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requesting a task identifier&lt;/td&gt;
&lt;td&gt;Whether the task runs at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setting &lt;code&gt;earliestBeginDate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;When the task actually runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handling the expiration callback&lt;/td&gt;
&lt;td&gt;How long the task gets to run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For &lt;code&gt;BGAppRefreshTask&lt;/code&gt;, your handler gets approximately 30 seconds. Apple enforces this with an expiration handler that fires when the budget runs out. The iOS scheduler in KMPWorker registers this expiration handler and emits &lt;code&gt;TaskState.TimedOut&lt;/code&gt; when it fires, rather than leaving the task in an indeterminate state.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Periodic&lt;/code&gt; task type maps to &lt;code&gt;BGProcessingTask&lt;/code&gt;, which typically only runs when the device is connected to power and idle. It gets a longer execution window than a refresh task, but comes with stricter system preconditions. There is currently no way to guarantee that a periodic KMPWorker task will run at a specific interval on iOS — the &lt;code&gt;repeatIntervalMillis&lt;/code&gt; value is a &lt;em&gt;hint to the system&lt;/em&gt;, not a contract.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cancelByTag()&lt;/code&gt; implementation on iOS is a good example of where the platform diverges from Android's model. WorkManager supports cancelling by tag natively. &lt;code&gt;BGTaskScheduler&lt;/code&gt; only supports cancellation by identifier. So &lt;code&gt;IOSKmpWorker.cancelByTag()&lt;/code&gt; currently cancels all registered tasks, which is a broader operation than what Android's implementation does. This is called out explicitly in the source with a warning log.&lt;/p&gt;

&lt;p&gt;If tag-level cancellation granularity matters for your use case on iOS, this is a limitation to design around.&lt;/p&gt;




&lt;h2&gt;
  
  
  State Broadcasting with TaskMonitor
&lt;/h2&gt;

&lt;p&gt;State changes flow through &lt;code&gt;TaskMonitor&lt;/code&gt;, a singleton that wraps a &lt;code&gt;MutableSharedFlow&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;states&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableSharedFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;replay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extraBufferCapacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;replay = 1&lt;/code&gt; is important: new collectors immediately receive the last emitted state for any task, without waiting for the next emission. This means a UI screen that navigates to a task-detail view after the task has already completed will still see &lt;code&gt;TaskState.Success&lt;/code&gt; rather than nothing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;extraBufferCapacity = 64&lt;/code&gt; prevents slow collectors from back-pressuring the emitters. A background task running in &lt;code&gt;KmpTaskWorker&lt;/code&gt; should never be blocked by a UI observer being slow to consume events.&lt;/p&gt;

&lt;p&gt;For apps that need state to survive process termination — common for sync tasks that need to surface completion even if the user relaunched the app — there's an optional &lt;code&gt;EventStore&lt;/code&gt; mechanism. Terminal states (&lt;code&gt;Success&lt;/code&gt;, &lt;code&gt;Cancelled&lt;/code&gt;, &lt;code&gt;Failed&lt;/code&gt; with &lt;code&gt;willRetry = false&lt;/code&gt;) are written to the store &lt;em&gt;before&lt;/em&gt; the in-memory emit, so even if the process dies immediately after writing, the event is safely on disk. &lt;code&gt;TaskMonitor.replayPendingEvents()&lt;/code&gt; is then called at app startup to rebroadcast any events that weren't delivered in the previous session.&lt;/p&gt;




&lt;h2&gt;
  
  
  Retry Engine
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;RetryEngine&lt;/code&gt; is stateless — a pure function that maps &lt;code&gt;(retryCount, RetryPolicy)&lt;/code&gt; to a delay in milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;RetryPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exponential&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;maxDelay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MAX_VALUE&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;shift&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;coerceIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;multiplier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1L&lt;/span&gt; &lt;span class="n"&gt;shl&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;  &lt;span class="c1"&gt;// 2^shift&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxDelay&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initialDelayMillis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;coerceAtLeast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;maxDelay&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initialDelayMillis&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;multiplier&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The overflow guard is deliberate. Without it, a long-running exponential backoff (say, 64+ retries) would overflow a &lt;code&gt;Long&lt;/code&gt; and produce a negative delay. The implementation caps at &lt;code&gt;Long.MAX_VALUE / 2&lt;/code&gt; — a safe practical ceiling that prevents arithmetic errors without complicating the calling code.&lt;/p&gt;

&lt;p&gt;The three available policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;RetryPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;None&lt;/span&gt;                                  &lt;span class="c1"&gt;// no retry&lt;/span&gt;
&lt;span class="nc"&gt;RetryPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delayMillis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;// fixed 5s between attempts&lt;/span&gt;
&lt;span class="nc"&gt;RetryPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exponential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;initialDelayMillis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;// attempt 1: 5s&lt;/span&gt;
    &lt;span class="n"&gt;maxRetries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;                                &lt;span class="c1"&gt;// attempt 2: 10s, 3: 20s, 4: 40s, 5: 80s&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Task Chains and Step Persistence
&lt;/h2&gt;

&lt;p&gt;For multi-step workflows where each step must complete before the next begins, &lt;code&gt;TaskChain&lt;/code&gt; provides a sequenced execution model. The chain executor (&lt;code&gt;TaskChainExecutor&lt;/code&gt;) observes &lt;code&gt;TaskMonitor.observeAll()&lt;/code&gt; and advances to the next step on success.&lt;/p&gt;

&lt;p&gt;What makes this non-trivial is crash safety. Before enqueueing step &lt;code&gt;n+1&lt;/code&gt;, the chain executor calls &lt;code&gt;chainRepository.updateStep(chain.id, nextStep, "RUNNING")&lt;/code&gt;. This means if the process is killed between step completions, &lt;code&gt;restorePendingChains()&lt;/code&gt; at the next launch will resume from the last committed step rather than restarting from step 0.&lt;/p&gt;

&lt;p&gt;Step task IDs are namespaced under the chain ID (&lt;code&gt;${chain.id}:step:${index}&lt;/code&gt;) to avoid collisions with independently scheduled tasks.&lt;/p&gt;

&lt;p&gt;The builder DSL makes common chains readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;kmpWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"onboarding"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChainPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;beginWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fetch-profile"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"upload-avatar"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;constraints&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Constraints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requiresInternet&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notify-server"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;retryPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RetryPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exponential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ChainPolicy.REPLACE&lt;/code&gt; cancels any existing chain with the same ID before starting a new one. &lt;code&gt;ChainPolicy.KEEP&lt;/code&gt; skips enqueue if a chain with that ID is already running. &lt;code&gt;ChainPolicy.ALLOW_DUPLICATE&lt;/code&gt; (the default) always enqueues, which is useful for chains where concurrent executions of different "runs" are intentional.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Experimental and What's Stable
&lt;/h2&gt;

&lt;p&gt;The DAG (Directed Acyclic Graph) execution API — which allows independent nodes to run in parallel while respecting declared dependencies — is marked &lt;code&gt;@OptIn(ExperimentalKmpWorkerApi::class)&lt;/code&gt;. This means the API surface may change between releases. The chain API and the core &lt;code&gt;KmpWorker&lt;/code&gt; interface are stable.&lt;/p&gt;

&lt;p&gt;The transfer module (&lt;code&gt;kmpworker-transfer&lt;/code&gt;) uses &lt;code&gt;HttpURLConnection&lt;/code&gt; on Android and &lt;code&gt;NSURLSession&lt;/code&gt; on iOS for resumable background downloads and uploads, without pulling in Ktor. This avoids adding a heavyweight dependency just for HTTP, but it also means the transfer module lacks Ktor's interceptor model and authentication abstractions. If your use case involves complex auth flows or middleware, you'd likely want to layer your own HTTP client on top of KMPWorker's task scheduling rather than using the transfer module directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Add the umbrella artifact or pick specific modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shared module build.gradle.kts&lt;/span&gt;
&lt;span class="n"&gt;commonMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.neuralheads.kmpworker:kmpworker-core:0.1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;androidMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.neuralheads.kmpworker:kmpworker-android:0.1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;iosMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.neuralheads.kmpworker:kmpworker-ios:0.1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Android initialization is handled automatically via &lt;code&gt;KmpWorkerInitializer&lt;/code&gt;, which uses the App Startup library to wire up the WorkManager factory without requiring any &lt;code&gt;Application&lt;/code&gt; subclass code.&lt;/p&gt;

&lt;p&gt;iOS requires explicit initialization in &lt;code&gt;AppDelegate&lt;/code&gt; before the app finishes launching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;kmpWorker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;IOSKmpWorker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;didFinishLaunchingWithOptions&lt;/span&gt; &lt;span class="nv"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;LaunchOptionsKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;kmpWorker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"sync"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* your work */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;kmpWorker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full module reference and documentation are in the &lt;a href="https://github.com/neuralheads/kmpworker" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Goes Next
&lt;/h2&gt;

&lt;p&gt;The core scheduling and chain execution is stable. The areas still under active development include the DAG executor (experimental), the Compose Multiplatform live inspector (&lt;code&gt;kmpworker-inspector&lt;/code&gt;), and expanding the transfer module's error handling. The next article in this series covers the offline queue and persistence architecture — specifically how SQLDelight is used to ensure tasks survive app termination and network disconnections.&lt;/p&gt;

&lt;p&gt;If you're building a KMP app that needs reliable background work across both platforms, KMPWorker gives you a starting point that handles the platform-specific wiring so your shared code does not have to.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;KMPWorker is published under the Apache 2.0 license. Source is available at &lt;a href="https://github.com/neuralheads/kmpworker" rel="noopener noreferrer"&gt;github.com/neuralheads/kmpworker&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The Utility-First Paradox: Structuring Massive UI Kits with Tailwind CSS and React 19</title>
      <dc:creator>vanced youtube</dc:creator>
      <pubDate>Wed, 15 Apr 2026 18:56:14 +0000</pubDate>
      <link>https://dev.to/realvancedyoutube/the-utility-first-paradox-structuring-massive-ui-kits-with-tailwind-css-and-react-19-n3g</link>
      <guid>https://dev.to/realvancedyoutube/the-utility-first-paradox-structuring-massive-ui-kits-with-tailwind-css-and-react-19-n3g</guid>
      <description>&lt;p&gt;Let’s be honest: Tailwind CSS is the "Wild West" of frontend development. It’s incredibly fast to iterate, but if you don't have a systemic approach to architecture, a project the size of ProofMatcher—with nine distinct premium templates and hundreds of components—quickly descends into a "class soup" nightmare. &lt;/p&gt;

&lt;p&gt;Most developers start by slapping utilities on every &lt;code&gt;div&lt;/code&gt; until it looks right. That works for a single landing page. But when you’re building a high-fidelity marketplace using &lt;strong&gt;React 19, Vite, and a decoupled Django backend&lt;/strong&gt;, you need a styling architecture that scales without bloating your bundle or making your components unreadable.&lt;/p&gt;

&lt;p&gt;Here is how we structured the styling engine for ProofMatcher to ensure performance and maintainability at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Design Tokens: The "Single Source of Truth"
&lt;/h2&gt;

&lt;p&gt;The biggest mistake you can make with Tailwind is relying on ad-hoc values (e.g., &lt;code&gt;text-[#c6f91f]&lt;/code&gt; or &lt;code&gt;p-[1.45rem]&lt;/code&gt;). In a massive UI kit, these arbitrary values are technical debt waiting to happen.&lt;/p&gt;

&lt;p&gt;We treat our &lt;code&gt;tailwind.config.js&lt;/code&gt; as the "Design System Bible." We don't just define colors; we define &lt;strong&gt;rhythm&lt;/strong&gt;. Every spacing value, blur intensity, and gradient angle is tokenized. For instance, in our &lt;em&gt;Neural Core&lt;/em&gt; template, we don't use a random cyan. We use &lt;code&gt;theme('colors.brand.primary')&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tailwind.config.js snippet&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;neon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#c6f91f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#05080A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(255, 255, 255, 0.1)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;backdropBlur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;glass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shimmer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shimmer 1.5s infinite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By strictly adhering to tokens, we can change the entire "vibe" of a template by updating one config file, rather than hunting through 50 &lt;code&gt;.tsx&lt;/code&gt; files.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Component Abstraction vs. Class Bloat
&lt;/h2&gt;

&lt;p&gt;The "Tailwind makes code messy" argument is usually a symptom of poor component abstraction. If your React component has 40 classes on a single &lt;code&gt;div&lt;/code&gt;, you probably haven't broken the component down far enough.&lt;/p&gt;

&lt;p&gt;In React 19, we lean heavily into &lt;strong&gt;Atomic Components&lt;/strong&gt;. Instead of styling a button every time, we build a &lt;code&gt;BeamButton&lt;/code&gt; or a &lt;code&gt;GoldBuyButton&lt;/code&gt;. We use the &lt;code&gt;tailwind-merge&lt;/code&gt; and &lt;code&gt;clsx&lt;/code&gt; pattern to handle dynamic class injection without the dreaded "class name collision" bug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;twMerge&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tailwind-merge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;clsx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ClassValue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClassValue&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;twMerge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;clsx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage in our marketplace&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-6 py-3 rounded-lg transition-all duration-300&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isPro&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-brand-neon text-black&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-white/5 text-white&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;className&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern allows us to keep the component logic separate from the "styling soup," while still maintaining the efficiency of Tailwind's JIT (Just-In-Time) compiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Death of &lt;code&gt;@apply&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you are using &lt;code&gt;@apply&lt;/code&gt; to create "component classes" in your CSS files, you are likely doing it wrong. While it looks cleaner, you lose the primary benefit of Tailwind: the ability to see exactly what a component looks like by looking at its markup. More importantly, &lt;code&gt;@apply&lt;/code&gt; balloons your final CSS bundle because it duplicates the actual CSS properties rather than reusing the atomic utility classes.&lt;/p&gt;

&lt;p&gt;At ProofMatcher, our &lt;code&gt;index.css&lt;/code&gt; is almost empty. We handle 99% of our styles via React props and utility classes. This ensures that Vite can perform optimal tree-shaking, only shipping the CSS that is actually active in the user's viewport.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Handling Transitions and State in React 19
&lt;/h2&gt;

&lt;p&gt;React 19’s new &lt;code&gt;useTransition&lt;/code&gt; and &lt;code&gt;useActionState&lt;/code&gt; hooks are a game-changer for UI feedback. We coordinate these states with Tailwind's &lt;code&gt;group&lt;/code&gt; and &lt;code&gt;peer&lt;/code&gt; utilities. &lt;/p&gt;

&lt;p&gt;For example, our &lt;em&gt;CheckoutPage&lt;/em&gt; uses "Loading States" that are entirely CSS-driven. When a Django DRF action is pending, we toggle a single &lt;code&gt;is-pending&lt;/code&gt; class on a parent container, and all child "skeleton" elements automatically trigger their shimmer animations via the &lt;code&gt;group-is-pending:animate-shimmer&lt;/code&gt; utility. This keeps the JavaScript layer focused on data and the styling layer focused on visuals.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Decoupled Styling: The Django Context
&lt;/h2&gt;

&lt;p&gt;When working with a decoupled Django backend, you often need to share "status colors" or "brand themes" across the API. We achieve this by serving Design Tokens as a JSON endpoint from Django, which we then inject into our CSS via &lt;strong&gt;CSS Variables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Tailwind handles these variables beautifully:&lt;br&gt;
&lt;code&gt;bg-[var(--user-theme-color)]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This allows us to support "Dynamic Branding" for our enterprise clients. The backend tracks the brand color; the frontend renders it with Tailwind's performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Performance: The 60fps Standard
&lt;/h2&gt;

&lt;p&gt;A "massive" UI kit often suffers from layout shifts and janky animations. We use Tailwind primarily for layout and static styling, but we delegate complex or high-frequency animations (like the 3D orbit gallery in our &lt;em&gt;Creative Agency&lt;/em&gt; template) to &lt;strong&gt;GSAP or Framer Motion&lt;/strong&gt;, controlled via React refs. &lt;/p&gt;

&lt;p&gt;The division of labor is clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind:&lt;/strong&gt; Layout, colors, spacing, and hover states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GSAP:&lt;/strong&gt; Narrative animations and coordinate-based movements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite:&lt;/strong&gt; Aggressive purging and chunking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By following this hierarchy, our marketplace maintains a 95+ performance score on mobile, even with high-fidelity 3D assets running in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: See the Blueprint
&lt;/h2&gt;

&lt;p&gt;Structuring a massive UI kit isn't about avoiding utility classes; it's about &lt;strong&gt;systematizing&lt;/strong&gt; them. It's about building a language where &lt;code&gt;neon&lt;/code&gt; means something specific across 500 components.&lt;/p&gt;

&lt;p&gt;If you want to see exactly how we handled the "class soup" problem in a real production environment, you don't need to take my word for it. We’ve open-sourced our &lt;strong&gt;Neural Core Interface&lt;/strong&gt;—the same tech stack we use to build our premium marketplace. &lt;/p&gt;

&lt;p&gt;Visit &lt;a href="https://proofmatcher.com" rel="noopener noreferrer"&gt;proofmatcher.com&lt;/a&gt; and download the &lt;strong&gt;Free UI Kit&lt;/strong&gt;. Dig into the code, check out the &lt;code&gt;tailwind.config.js&lt;/code&gt;, and see how we’ve utilized React 19 and Vite to create a lightning-fast, highly-maintainable styling system. Stop writing messy CSS and start building systems.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>react</category>
      <category>tailwindcss</category>
      <category>ui</category>
    </item>
    <item>
      <title>Why We Killed CRA: Building a High-Performance Marketplace with React 19 + Vite + Django</title>
      <dc:creator>vanced youtube</dc:creator>
      <pubDate>Sun, 12 Apr 2026 13:58:49 +0000</pubDate>
      <link>https://dev.to/realvancedyoutube/why-we-killed-cra-building-a-high-performance-marketplace-with-react-19-vite-django-d7m</link>
      <guid>https://dev.to/realvancedyoutube/why-we-killed-cra-building-a-high-performance-marketplace-with-react-19-vite-django-d7m</guid>
      <description>&lt;p&gt;Let me be real: if you're still using &lt;code&gt;create-react-app&lt;/code&gt; for production apps in 2024, you're fighting your own tooling.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://proofmatcher.com" rel="noopener noreferrer"&gt;ProofMatcher&lt;/a&gt;, we run a marketplace for &lt;strong&gt;premium website templates&lt;/strong&gt; — think animated, interactive &lt;strong&gt;modern web templates&lt;/strong&gt; with live previews. Our stack? &lt;strong&gt;React 19, Vite, Tailwind, and Django REST Framework&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's why we ditched CRA and what we learned shipping this to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: CRA Can't Keep Up
&lt;/h2&gt;

&lt;p&gt;When you're building a template marketplace, you're basically shipping a gallery of mini-apps. Each template has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live previews (often with Three.js or GSAP)&lt;/li&gt;
&lt;li&gt;Custom animations&lt;/li&gt;
&lt;li&gt;Heavy asset loading&lt;/li&gt;
&lt;li&gt;SEO requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CRA's dev server started choking around 20+ templates. Cold starts took 30+ seconds. HMR felt random. Our team was spending more time waiting than coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Vite Won
&lt;/h2&gt;

&lt;p&gt;Vite's dev server uses ESM natively. No more bundling everything upfront. Your browser fetches modules as needed.&lt;/p&gt;

&lt;p&gt;The difference is night and day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold starts&lt;/strong&gt;: ~1s vs 30s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HMR updates&lt;/strong&gt;: Instant vs "is it working?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory usage&lt;/strong&gt;: ~500MB vs 2GB+&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's our base config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;manualChunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;three&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;three&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;framer-motion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lucide-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  React 19 in Production
&lt;/h2&gt;

&lt;p&gt;Yes, we're running React 19. No, it's not "unstable." The React team's been releasing canary builds for ages, and the Vite plugin handles it beautifully.&lt;/p&gt;

&lt;p&gt;The real win? &lt;strong&gt;React Server Components aren't mandatory.&lt;/strong&gt; You can use the new features (like Actions) without buying into the full RSC architecture.&lt;/p&gt;

&lt;p&gt;Our async setup with Django:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Using React 19's use() with Suspense&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/templates/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TemplatePreview&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetchTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;iframe&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previewUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Wrap with Suspense where used&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TemplateSkeleton&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TemplatePreview&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Suspense&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Django + Vite: The Proxy Magic
&lt;/h2&gt;

&lt;p&gt;We keep frontend and backend completely separate. Django handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication (JWT via SimpleJWT)&lt;/li&gt;
&lt;li&gt;Database models&lt;/li&gt;
&lt;li&gt;Stripe webhooks&lt;/li&gt;
&lt;li&gt;File uploads (S3 presigned URLs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vite proxies API requests during development, so no CORS issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In Django settings.py&lt;/span&gt;
&lt;span class="nx"&gt;CORS_ALLOWED_ORIGINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:5173&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Vite&lt;/span&gt; &lt;span class="nx"&gt;dev&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://proofmatcher.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;And&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;production&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nx"&gt;STATICFILES_DIRS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;frontend-dist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Built&lt;/span&gt; &lt;span class="nx"&gt;Vite&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Wins That Matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Chunking Strategy
&lt;/h3&gt;

&lt;p&gt;We manually split Three.js from our main bundle. Users don't pay for 3D unless they view a 3D template.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Image Optimization
&lt;/h3&gt;

&lt;p&gt;Using Vite's built-in image handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;templateImage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./assets/template.jpg?w=800&amp;amp;format=webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="c1"&gt;// Generates multiple sizes, webp format&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. CSS That Doesn't Block
&lt;/h3&gt;

&lt;p&gt;Tailwind + Vite's JIT mode means CSS updates are instant. No more PostCSS rebuild delays.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Iframe Preview Solution
&lt;/h2&gt;

&lt;p&gt;This was our trickiest problem: how to preview templates without style collisions.&lt;/p&gt;

&lt;p&gt;Solution: Each preview runs in a sandboxed iframe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Template preview component --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"preview-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt;
    &lt;span class="na"&gt;sandbox=&lt;/span&gt;&lt;span class="s"&gt;"allow-scripts allow-same-origin"&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;{`/template-previews/${templateId}/`}&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite builds these previews as separate mini-apps in &lt;code&gt;/public&lt;/code&gt;. Zero runtime cost for the main app.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO Without SSR
&lt;/h2&gt;

&lt;p&gt;We get this question a lot: "How do you handle SEO without Next.js?"&lt;/p&gt;

&lt;p&gt;Honestly? Good old &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags and smart URL design. Django pre-renders critical pages (homepage, categories), and React takes over after load.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;SEOHead&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SEOHead&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;ProofMatcher&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/title&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;og:image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canonical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a marketplace, Google cares more about page speed and user experience than perfect SSR. Our Lighthouse scores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance: 98&lt;/li&gt;
&lt;li&gt;SEO: 100&lt;/li&gt;
&lt;li&gt;Accessibility: 97&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Would We Do It Again?
&lt;/h2&gt;

&lt;p&gt;Absolutely. Our developer experience improved dramatically:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;CRA&lt;/th&gt;
&lt;th&gt;Vite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dev server start&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMR update&lt;/td&gt;
&lt;td&gt;2-5s&lt;/td&gt;
&lt;td&gt;&amp;lt;100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production build&lt;/td&gt;
&lt;td&gt;4min&lt;/td&gt;
&lt;td&gt;45s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size (homepage)&lt;/td&gt;
&lt;td&gt;450KB&lt;/td&gt;
&lt;td&gt;180KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The team ships features faster. Our templates load quicker. Users stay longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started Yourself
&lt;/h2&gt;

&lt;p&gt;If you want to try this stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Frontend&lt;/span&gt;
npx create-vite@latest myapp &lt;span class="nt"&gt;--template&lt;/span&gt; react
&lt;span class="nb"&gt;cd &lt;/span&gt;myapp
npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Backend&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;django djangorestframework django-cors-headers
django-admin startproject backend &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out &lt;a href="https://vitejs.dev" rel="noopener noreferrer"&gt;Vite's docs&lt;/a&gt; and &lt;a href="https://www.django-rest-framework.org/" rel="noopener noreferrer"&gt;Django REST Framework&lt;/a&gt;. Both have fantastic communities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;CRA served its purpose. But for modern apps with complex UIs, Vite is just better. Faster dev server, smaller bundles, better DX.&lt;/p&gt;

&lt;p&gt;We're &lt;a href="https://proofmatcher.com/templates/webgl-animation-page" rel="noopener noreferrer"&gt;open sourcing our base template&lt;/a&gt; if you want to see the exact setup. It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 19 + Vite config&lt;/li&gt;
&lt;li&gt;Django API setup&lt;/li&gt;
&lt;li&gt;Three.js chunking&lt;/li&gt;
&lt;li&gt;Iframe preview system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's your stack look like? Still on CRA or moved to something else?&lt;/p&gt;

</description>
      <category>django</category>
      <category>performance</category>
      <category>react</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
