<?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: Beksultan Sagnaev</title>
    <description>The latest articles on DEV Community by Beksultan Sagnaev (@beksnotfound).</description>
    <link>https://dev.to/beksnotfound</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%2F1547453%2F76f799eb-c99c-4542-ba56-f0806b9b0077.jpg</url>
      <title>DEV Community: Beksultan Sagnaev</title>
      <link>https://dev.to/beksnotfound</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/beksnotfound"/>
    <language>en</language>
    <item>
      <title>How We Turned a High-Traffic News Site's Core Web Vitals from Red to Green</title>
      <dc:creator>Beksultan Sagnaev</dc:creator>
      <pubDate>Tue, 14 Jul 2026 15:19:21 +0000</pubDate>
      <link>https://dev.to/beksnotfound/how-we-turned-a-high-traffic-news-sites-core-web-vitals-from-red-to-green-3h14</link>
      <guid>https://dev.to/beksnotfound/how-we-turned-a-high-traffic-news-sites-core-web-vitals-from-red-to-green-3h14</guid>
      <description>&lt;p&gt;&lt;strong&gt;The setup: a news site where every millisecond is public&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kapital.kz is one of Kazakhstan's high-traffic business and finance news portals, built on Next.js. Unlike a typical marketing site, a news portal has a brutal constraint: every page is a landing page. Every article is someone's entry point from Google, and Google's Core Web Vitals directly influence how well those articles rank. A slow, janky article page doesn't just annoy readers — it actively pushes the site down in search results.&lt;/p&gt;

&lt;p&gt;When I joined the team at Musan Group, all three Core Web Vitals metrics — LCP, INP, and CLS — were sitting in the red zone on &lt;code&gt;finance.kapital.kz&lt;/code&gt;. For context, Google's thresholds put "poor" LCP above 4.0s (versus "good" at 2.5s or under), "poor" INP above 500ms (versus 200ms or under), and "poor" CLS above 0.25 (versus 0.1 or under) — that's the red-to-green gap we were closing. Here's what that actually meant in practice, and what it took to fix each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LCP: the hero image problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Largest Contentful Paint measures how long it takes for the biggest visible element — usually the hero image or headline — to render. On a news site, that's almost always the article's cover image.&lt;br&gt;
The fixes here were less about clever tricks and more about removing waste: cutting unnecessary client-side work that delayed the image request, and making sure the image itself wasn't fighting other resources for bandwidth on first load. Combined with general rendering path cleanup, this brought LCP on &lt;code&gt;finance.kapital.kz&lt;/code&gt; down out of the red zone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;INP: the interaction that made the page feel broken&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interaction to Next Paint&lt;/strong&gt; replaced FID as a Core Web Vital, and it measures something more honest: not just "did the first click register fast," but "does every interaction on this page feel instant, for the whole time the user is on it." A page can have great LCP and still feel broken if scrolling stutters or a click takes half a second to respond.&lt;/p&gt;

&lt;p&gt;Two things were quietly wrecking INP on article pages:&lt;br&gt;
&lt;strong&gt;Google Ads scripts loaded incorrectly.&lt;/strong&gt; Ad scripts are notorious for blocking the main thread — if they're not loaded with the right strategy (deferred, lazy, or isolated from critical rendering work), every ad slot on the page becomes a tax on every single interaction, not just page load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heavy libraries sitting in the main bundle.&lt;/strong&gt; Some dependencies had no business being loaded upfront on every article page. Moving them out of the initial JS bundle — loading them only when actually needed — freed up the main thread to actually respond to user input instead of parsing and executing code nobody was using yet. One concrete example: a heavy date/time-picker library used only on category-navigation pages was loading globally, on every single article page — including the ones that never used it. Swapping it for the lightest available alternative, with no visible UI trade-off, cut dead weight out of the main bundle entirely.&lt;/p&gt;

&lt;p&gt;Between fixing the ad loading strategy and pulling heavy libraries out of the critical path, INP moved into the green zone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLS: the classic culprits — ads and infinite scroll&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cumulative Layout Shift&lt;/strong&gt; measures visual stability — how much content jumps around as the page loads. For a news site, the two biggest offenders are almost always the same:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ads without reserved space.&lt;/strong&gt; When an ad slot doesn't have a fixed size reserved before the ad itself loads, the page content jumps down the moment the ad finally renders — classic CLS violation, and one of the most common on ad-supported sites specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infinite scroll done carelessly.&lt;/strong&gt; As new articles load in below the fold, any miscalculation in how their height is estimated before the images finish loading causes the whole feed to jitter and shift under the reader's thumb. One visible symptom: the site's global footer would jump up and down every time a new batch of articles loaded, because the page's total height kept getting recalculated on the fly.&lt;/p&gt;

&lt;p&gt;Fixing both — properly reserving space for ad units before they load, and correcting how infinite-scroll batches estimate their height before content is fully rendered — eliminated the layout shift almost entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding the problems in the first place: BigQuery over guesswork&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the part that made the difference between guessing at fixes and actually targeting the right ones: instead of relying only on lab data (Lighthouse scores, which are a single synthetic run), the team pulled &lt;strong&gt;real user monitoring data through BigQuery&lt;/strong&gt; — actual field data from actual readers, on actual devices and connections across Kazakhstan.&lt;/p&gt;

&lt;p&gt;This matters because lab data lies by omission. A Lighthouse run on a fast office connection tells you nothing about what a reader on a mid-range Android phone over a spotty mobile connection in a smaller city actually experiences. BigQuery let us see the real distribution of LCP/INP/CLS across the actual user base, and — critically — segment it: which specific pages, which devices, which network conditions were dragging the aggregate numbers down. That turned "the site feels slow" into a ranked list of specific, fixable bottlenecks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other half of technical SEO: making Google understand the content, not just render it fast&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Core Web Vitals is only one pillar. The other half of technical SEO for a news site is making sure search engines can correctly parse what an article actually is — and this is where structured data and AMP come in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updating schema.org JSON-LD&lt;/strong&gt; for news articles matters more than it sounds like it should. Structured data is how Google knows "this is a NewsArticle, published on this date, by this author, about this topic" instead of just guessing from raw HTML. Getting the JSON-LD schema right and current directly affects whether an article is eligible for rich results in search — the difference between a plain blue link and one with a headline, image, and publish date attached in the results page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixing broken AMP versions&lt;/strong&gt; closed a gap that's easy to overlook: if a site publishes both a regular and an AMP version of each article, and the AMP version is broken or out of sync, search engines either serve a degraded experience or lose trust in the AMP feed altogether — quietly hurting visibility for the very pages meant to load fastest on mobile search.&lt;/p&gt;

&lt;p&gt;Alongside this, cleaning up HTML validation issues and fixing production bugs in the news card UI closed out the more mundane but still SEO-relevant half of the work: broken markup and inconsistent card rendering both chip away at how reliably a page renders — and gets crawled — across different devices and bots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;None of these fixes were exotic. Reserve space for ads. Defer what doesn't need to run immediately. Get your structured data right. Use field data, not just lab scores, to know what to fix first. What made it work wasn't a single trick — it was treating Core Web Vitals as a measurement problem first (find the real bottleneck via BigQuery) and a discipline problem second (systematically closing each gap: rendering path, ad loading, layout stability, structured data, AMP).&lt;/p&gt;

&lt;p&gt;For a site where every article is a search-engine entry point, that discipline is the difference between ranking and not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work as a full-stack developer based in Astana, Kazakhstan, focused on Next.js, technical SEO, and performance optimization for high-traffic products. If you're dealing with Core Web Vitals or technical SEO issues on your own site, I'd be glad to compare notes — reach me on &lt;a href="https://t.me/beksnotfound" rel="noopener noreferrer"&gt;Telegram&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/beksultan-sagnaev/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, or check out more of my work at &lt;a href="https://beksnotfound.kz/en" rel="noopener noreferrer"&gt;beksnotfound.kz&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webperf</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Building a React Native SDK Inside a Super-App with 800K Daily Users</title>
      <dc:creator>Beksultan Sagnaev</dc:creator>
      <pubDate>Fri, 10 Jul 2026 15:15:24 +0000</pubDate>
      <link>https://dev.to/beksnotfound/building-a-react-native-sdk-inside-a-super-app-with-800k-daily-users-46mm</link>
      <guid>https://dev.to/beksnotfound/building-a-react-native-sdk-inside-a-super-app-with-800k-daily-users-46mm</guid>
      <description>&lt;p&gt;&lt;strong&gt;The problem: an SDK is not an app&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most React Native tutorials teach you to build a standalone app. You control the root navigation, the splash screen, the lifecycle — everything. But what happens when your React Native code has to live inside someone else's native app, one you don't control and definitely can't crash?&lt;/p&gt;

&lt;p&gt;That's the brief I got at Kwaaka: embed a restaurant-coupons module into &lt;a href="https://onay.kz" rel="noopener noreferrer"&gt;&lt;strong&gt;Onay&lt;/strong&gt;&lt;/a&gt;, a Kazakhstan-wide super-app with roughly 800,000 daily active users. Onay users needed to discover nearby restaurant discounts without installing a separate app — the coupon experience had to feel native to an app we never touched the codebase of.&lt;br&gt;
I was the sole frontend developer on this: architecture, performance, and shipping were entirely on me, working closely with Kwaaka's backend team and Onay's iOS/Android engineers who wired the SDK into their production app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why React Native (and not native-native)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kwaaka's whole product suite runs on React on the web. Choosing React Native meant the team could share mental models and logic between web and mobile without hiring separate iOS/Android frontend specialists. State management ran on Effector — again, for consistency with Kwaaka's web stack, plus it's well-documented and widely used across the CIS developer community. In my experience, &lt;a href="https://effector.dev/" rel="noopener noreferrer"&gt;&lt;strong&gt;Effector&lt;/strong&gt;&lt;/a&gt; is a genuinely simpler and more modern state manager than old-guard &lt;strong&gt;Redux&lt;/strong&gt; (or Redux Toolkit, for that matter) — no action types to name, no reducers to switch over, no selectors to memoize by hand. You just declare events and stores, and derive everything else from them. Less boilerplate, less ceremony, and the dependency graph between stores stays explicit instead of hiding behind &lt;code&gt;connect()&lt;/code&gt; or &lt;code&gt;useSelector&lt;/code&gt;. Farfetched sat on top of Effector to handle caching, retries, and stale-while-revalidate out of the box.&lt;/p&gt;

&lt;p&gt;The constraint that shaped everything: an SDK has no root navigation, no lifecycle of its own, and cannot crash the host app. A single unhandled JS exception inside a normal React Native app just crashes that app. Inside an SDK, it would take down Onay itself, for 800K people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shipping fixes without App Store review&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Native review cycles are slow, and coupon logic changes fast. So I built a custom over-the-air update system: the JS bundle gets built, pushed to Cloudflare R2, and picked up by the SDK on next launch — no App Store review, no native app update required.&lt;br&gt;
The system runs on two tiers:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;latest.json&lt;/code&gt; — checked roughly every 30 minutes for routine updates&lt;br&gt;
&lt;code&gt;critical.json&lt;/code&gt; — a lightweight signal file checked every ~5 minutes; it doesn't carry the fix itself, it just tells the SDK "go check &lt;code&gt;latest.json&lt;/code&gt; right now"&lt;/p&gt;

&lt;p&gt;Every bundle is verified with a SHA-256 hash before being applied. If the native SDK version changes, the cache invalidates automatically, so an old JS bundle never tries to call native APIs that no longer exist. A small CI/CD script handles the whole pipeline: build → upload to R2 → update manifests → invalidate CDN cache. One script run, and the fix reaches every user with zero store releases.&lt;br&gt;
Bridging into a host app without asking users to log in again&lt;br&gt;
Onay users are already authenticated in the native app. Rather than showing a login screen inside the SDK, I pull the JWT, coordinates, and language straight from the native layer via NativeModules. From the user's perspective, the coupon module just opens — no second login, no friction.&lt;/p&gt;

&lt;p&gt;Getting there meant close coordination with Onay's iOS and Android teams: agreeing on the native bridge API, the SDK's lifecycle, deep-link handling, and how the modal should dismiss. Neither side changed their existing architecture — the SDK had to fit into Onay's existing flow, not the other way around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The platform-specific glue: Swift and Kotlin&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On iOS, the public API is a single line for the host app:&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="n"&gt;swiftCouponsSDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;viewController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;onTokenRefresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Underneath, a singleton bridge manager caches the RCT bridge by token — same token, same bridge instance reused; new token, fresh bridge. The SDK ships as an XCFramework via Swift Package Manager.&lt;br&gt;
One detail I'm particularly glad I built: a temporary override of &lt;code&gt;RCTFatalHandler&lt;/code&gt;. If the SDK's JS throws something unhandled, instead of crashing the host app, the SDK swaps in its own fatal handler, shows an error screen with a "Close" button, and restores the original handler on dismiss. Without this, any JS bug becomes an Onay crash.&lt;/p&gt;

&lt;p&gt;Android mirrors the same ideas with its own constraints — a &lt;code&gt;warmUp(application, token)&lt;/code&gt; call pre-warms the bridge on a daemon thread (roughly 60-80MB RAM, non-blocking), and the token gets read from the launching Intent before &lt;code&gt;super.onCreate()&lt;/code&gt; so the RN bridge has it ready before the first render.&lt;/p&gt;

&lt;p&gt;Both platforms share the same token-refresh contract: on a 401 from the coupons backend, the SDK calls back into native code, waits up to 30 seconds for a fresh token, retries once, and closes itself if that also fails. The host app doesn't know any of this is happening — it just supplies a callback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging without a safety net&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Being the only frontend developer on a project means there's no code review above you, no second opinion before you merge. That's not a comfort claim — it's a constraint that forces you to think two steps ahead, because a wrong architectural call has no one to catch it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two bugs stood out as genuinely instructive:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The re-rendering header.&lt;/strong&gt; Swiping between coupons was reloading the restaurant page's entire header — hero image, logo, info block — on every swipe. The chain: switching a coupon updated &lt;code&gt;localActiveCouponId&lt;/code&gt;, which caused &lt;code&gt;renderListHeader&lt;/code&gt; to be recreated, which remounted the coupon slider pager, which reset initialScrollIndex back to the first slide. The fix was to memoize the static parts of the header so the JSX object stayed stable, and move the coupon detail sections out of the header component into the &lt;code&gt;SectionList's&lt;/code&gt; own sections — so &lt;code&gt;renderListHeader&lt;/code&gt; no longer depended on the active coupon at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The empty home screen.&lt;/strong&gt; After switching location away from Almaty and back, the home page stayed blank. Farfetched was serving cached — and empty — data for anything under 30 seconds old, skipping the network call entirely. The fix: call &lt;code&gt;refresh()&lt;/code&gt; instead of &lt;code&gt;start()&lt;/code&gt; on coordinate changes, which deliberately bypasses the cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The coupon logic itself&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This turned out to be one of the trickiest parts of the whole project. Six coupon types — percentage off, fixed amount, free gift, paid gift, 2-for-1, and X+Y bundles — each with its own combination of unlock conditions: minimum order value, minimum item count, presence of specific products, or a minimum number of unique line items. All of it validated client-side in real time.&lt;/p&gt;

&lt;p&gt;A widget I'm fairly happy with, &lt;code&gt;CouponThresholdHint&lt;/code&gt;, shows a live progress bar and exact copy for whatever's still missing ("add 2 more items" vs. "add 1500₸ more"), correctly declined for Russian grammar (1 item / 2 items / 5 items follow different noun cases). Once the condition is met, it flips into a success state with a different visual treatment.&lt;/p&gt;

&lt;p&gt;Checkout runs double validation: client-side first (to block the button and explain the problem before any request goes out), then server-side (&lt;code&gt;validateCouponCart&lt;/code&gt;) right before order creation — if the backend rejects it, the error surfaces directly in the payment sheet. For gift and bundle coupons, the frontend has to construct part of the payload itself: resolving which product is the "free" one from the coupon's conditions, calculating quantity, and injecting it into the cart items before submission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it stands&lt;/strong&gt;&lt;br&gt;
The SDK is live in a test rollout in Almaty, with the first partner locations connected and a geo-based coupon feed keyed to the user's coordinates. The architecture was deliberately built so that adding a new city doesn't require any client-side changes. It supports Russian, Kazakh, and English, auto-detected from the JWT and switchable on the fly.&lt;/p&gt;

&lt;p&gt;Distribution through Onay solved the hardest problem a new product usually faces: building an audience from zero. The 800K users were already there — the job was making sure the coupon experience felt like it belonged.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work as a full-stack developer based in Astana, Kazakhstan, focused on React, React Native, and mobile SDK integrations. If you're dealing with a similar "embed our thing inside someone else's app" problem, I'd be glad to compare notes — reach me on &lt;a href="https://t.me/beksnotfound" rel="noopener noreferrer"&gt;Telegram&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/beksultan-sagnaev/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or check out more of my work at &lt;a href="https://beksnotfound.kz/en" rel="noopener noreferrer"&gt;beksnotfound.kz&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>javascript</category>
      <category>architecture</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
