<?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: Mai Chi Bao</title>
    <description>The latest articles on DEV Community by Mai Chi Bao (@mrzaizai2k).</description>
    <link>https://dev.to/mrzaizai2k</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg</url>
      <title>DEV Community: Mai Chi Bao</title>
      <link>https://dev.to/mrzaizai2k</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrzaizai2k"/>
    <language>en</language>
    <item>
      <title>I’ll be honest with you:
I didn’t wake up one morning thinking “Let me reverse-engineer TikTok today.”

It started with something much simpler:

“I just want to upload videos to TikTok programmatically…
why is this so damn hard?”</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Mon, 17 Nov 2025 14:05:37 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/ill-be-honest-with-you-i-didnt-wake-up-one-morning-thinking-let-me-reverse-engineer-tiktok-1pbg</link>
      <guid>https://dev.to/mrzaizai2k/ill-be-honest-with-you-i-didnt-wake-up-one-morning-thinking-let-me-reverse-engineer-tiktok-1pbg</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mrzaizai2k/breaking-down-api-defenses-ua-cookies-signatures-browser-5f58" class="crayons-story__hidden-navigation-link"&gt;Breaking Down API Defenses: UA - Cookies - Signatures Browser 🤖&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mrzaizai2k" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" alt="mrzaizai2k profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mrzaizai2k" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mai Chi Bao
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mai Chi Bao
                
              
              &lt;div id="story-author-preview-content-3021834" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mrzaizai2k" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mai Chi Bao&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mrzaizai2k/breaking-down-api-defenses-ua-cookies-signatures-browser-5f58" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 17 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mrzaizai2k/breaking-down-api-defenses-ua-cookies-signatures-browser-5f58" id="article-link-3021834"&gt;
          Breaking Down API Defenses: UA - Cookies - Signatures Browser 🤖
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mrzaizai2k"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mrzaizai2k&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/mrzaizai2k/breaking-down-api-defenses-ua-cookies-signatures-browser-5f58" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;12&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/mrzaizai2k/breaking-down-api-defenses-ua-cookies-signatures-browser-5f58#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              3&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>webdev</category>
      <category>api</category>
      <category>security</category>
      <category>mrzaizai2k</category>
    </item>
    <item>
      <title>Breaking Down API Defenses: UA - Cookies - Signatures Browser 🤖</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Mon, 17 Nov 2025 13:06:00 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/breaking-down-api-defenses-ua-cookies-signatures-browser-5f58</link>
      <guid>https://dev.to/mrzaizai2k/breaking-down-api-defenses-ua-cookies-signatures-browser-5f58</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I’ll be honest with you:&lt;br&gt;
I didn’t wake up one morning thinking &lt;em&gt;“Let me reverse-engineer TikTok today.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It started with something much simpler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I just want to upload videos to TikTok programmatically…&lt;br&gt;
why is this so damn hard?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No matter what I tried — simple requests, faked headers, even stolen cookies — TikTok kept slamming the door in my face.&lt;/p&gt;

&lt;p&gt;But every failure taught me something.&lt;br&gt;
Every blocked request revealed the next security layer.&lt;/p&gt;

&lt;p&gt;So I dug in.&lt;br&gt;
I became the “hacker” TikTok didn’t want.&lt;/p&gt;

&lt;p&gt;And in this post, I’ll take you through &lt;strong&gt;the exact journey&lt;/strong&gt; — from the most naive HTTP request… all the way to &lt;strong&gt;running TikTok’s own obfuscated JavaScript in a Node VM&lt;/strong&gt; to generate browser signatures.&lt;/p&gt;

&lt;p&gt;All code here comes from my real TikTok automation stack (based on my repo:&lt;br&gt;
&lt;a href="https://github.com/mrzaizai2k/auto_tiktok/blob/main/src/tiktok_uploader/tiktok.py" rel="noopener noreferrer"&gt;github.com/mrzaizai2k/auto_tiktok&lt;/a&gt;).&lt;/p&gt;


&lt;h1&gt;
  
  
  Table of Contents
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Raw Request — My First Dumb Attempt&lt;/li&gt;
&lt;li&gt;User-Agent — “Fine, I’ll Pretend to Be Chrome”&lt;/li&gt;
&lt;li&gt;Cookies — The Real Passport&lt;/li&gt;
&lt;li&gt;Browser Signatures — When TikTok Got Serious&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;li&gt;Final Thoughts&lt;/li&gt;
&lt;li&gt;Resources&lt;/li&gt;
&lt;/ol&gt;


&lt;h1&gt;
  
  
  Raw Request
&lt;/h1&gt;
&lt;h2&gt;
  
  
  🧠 What I Tried
&lt;/h2&gt;

&lt;p&gt;The first time I tried calling TikTok’s API, I thought it would be as easy as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.tiktok.com/api/whatever&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TikTok looked at me like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Bro, who even are you?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No headers.&lt;br&gt;
No cookies.&lt;br&gt;
No browser identity.&lt;br&gt;
Instant block.&lt;/p&gt;

&lt;p&gt;To understand why, I built a minimal FastAPI to simulate what TikTok sees when a bot sends a naked request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&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="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_data&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="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# no checks, no identity
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how the early internet worked.&lt;br&gt;
And this is why it died.&lt;/p&gt;


&lt;h1&gt;
  
  
  User-Agent
&lt;/h1&gt;
&lt;h3&gt;
  
  
  “I’ll wear Chrome’s clothes… surely that will work.”
&lt;/h3&gt;

&lt;p&gt;I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My FastAPI simulation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_data&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="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ua&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing UA&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And my crawler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked…&lt;br&gt;
…for exactly 5 minutes.&lt;/p&gt;

&lt;p&gt;Because &lt;strong&gt;any idiot can fake a UA&lt;/strong&gt;.&lt;br&gt;
Including me.&lt;/p&gt;

&lt;p&gt;So TikTok uses the next level.&lt;/p&gt;


&lt;h1&gt;
  
  
  Cookies
&lt;/h1&gt;
&lt;h2&gt;
  
  
  “Fine, TikTok, you want identity? Here’s my passport.”
&lt;/h2&gt;

&lt;p&gt;Cookies are &lt;strong&gt;small pieces of data the server sets in your browser&lt;/strong&gt;.&lt;br&gt;
When I login to TikTok on Chrome, the browser stores cookies like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sessionid&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msToken&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tt-target-idc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;csrf_session_id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren’t random strings — they’re TikTok’s way of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This browser is logged in. We know this guy.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To get these cookies, I use this extension (the best one I’ve found):&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://www.junookyo.com/2017/07/j2team-cookies-chrome-extension.html?utm_source=extension&amp;amp;utm_medium=j2team_cookies" rel="noopener noreferrer"&gt;J2Team Cookies (Chrome)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It exports cookies into a neat JSON file — perfect for automation.&lt;/p&gt;

&lt;p&gt;Then in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cookies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_cookies_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cookies/tiktok.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessionid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.tiktok.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tt-target-idc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.tiktok.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now TikTok was finally willing to talk to me.&lt;br&gt;
For a while.&lt;/p&gt;

&lt;p&gt;Because cookies can still be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stolen&lt;/li&gt;
&lt;li&gt;copied&lt;/li&gt;
&lt;li&gt;reused&lt;/li&gt;
&lt;li&gt;shared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TikTok needed something harder.&lt;/p&gt;


&lt;h1&gt;
  
  
  Browser Signatures
&lt;/h1&gt;
&lt;h2&gt;
  
  
  “Prove you’re a real browser or get lost.”
&lt;/h2&gt;

&lt;p&gt;This is where TikTok gets nasty.&lt;/p&gt;

&lt;p&gt;Even with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Valid cookies&lt;/li&gt;
&lt;li&gt;Valid User-Agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TikTok still blocks you unless you generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;X-Bogus&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_signature&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;msToken&lt;/code&gt; validation&lt;/li&gt;
&lt;li&gt;device/environment fingerprints&lt;/li&gt;
&lt;li&gt;obfuscated JS crypto outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TikTok literally sends JavaScript that only a &lt;strong&gt;real browser&lt;/strong&gt; can execute.&lt;/p&gt;

&lt;p&gt;So to bypass it…&lt;br&gt;
I used their own JavaScript.&lt;/p&gt;
&lt;h3&gt;
  
  
  👉 Running TikTok’s JS in a Node VM
&lt;/h3&gt;

&lt;p&gt;This is my real &lt;code&gt;browser.js&lt;/code&gt; (the one doing the magic):&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;// Browser.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./index&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;userAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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;signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Signer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&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;sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&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="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;And this is how my Python script calls it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subprocess_jsvmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;node&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PIPE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every API request needs this signature.&lt;br&gt;
Without it — TikTok spits on you.&lt;/p&gt;

&lt;p&gt;With it — you suddenly become “a real browser.”&lt;/p&gt;

&lt;p&gt;This is exactly how I upload videos automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;signatures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;subprocess_jsvmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sig_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signatures&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Bogus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-bogus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signature&lt;/span&gt;&lt;span class="sh"&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;Now TikTok believes me.&lt;br&gt;
And lets me upload videos through automation.&lt;/p&gt;




&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What I Did&lt;/th&gt;
&lt;th&gt;Why It Failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw request&lt;/td&gt;
&lt;td&gt;Called API directly&lt;/td&gt;
&lt;td&gt;“Who are you?”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User-Agent&lt;/td&gt;
&lt;td&gt;Pretended to be Chrome&lt;/td&gt;
&lt;td&gt;Too easy to fake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookies&lt;/td&gt;
&lt;td&gt;Exported real TikTok cookies&lt;/td&gt;
&lt;td&gt;Reusable → insecure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser Signatures&lt;/td&gt;
&lt;td&gt;Ran TikTok’s own JS&lt;/td&gt;
&lt;td&gt;Finally works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;There are even more authentication methods out there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth&lt;/li&gt;
&lt;li&gt;JWT tokens&lt;/li&gt;
&lt;li&gt;PKCE&lt;/li&gt;
&lt;li&gt;Rotating keys&lt;/li&gt;
&lt;li&gt;Device-bound tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want, I can continue this series:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Hit follow + drop a comment&lt;/strong&gt; if you want the next chapter.&lt;/p&gt;




&lt;h1&gt;
  
  
  Resources
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;My TikTok uploader code:
&lt;strong&gt;&lt;a href="https://github.com/mrzaizai2k/auto_tiktok/blob/main/src/tiktok_uploader/tiktok.py" rel="noopener noreferrer"&gt;auto_tiktok → tiktok.py&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>security</category>
      <category>mrzaizai2k</category>
    </item>
    <item>
      <title>Everything worked perfectly on my laptop. 🚀 The UI loaded, uploads succeeded, APIs responded instantly.

But on demo day, in front of my boss, everything collapsed. 💥</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Wed, 01 Oct 2025 02:55:55 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/everything-worked-perfectly-on-my-laptop-the-ui-loaded-uploads-succeeded-apis-responded-4jcj</link>
      <guid>https://dev.to/mrzaizai2k/everything-worked-perfectly-on-my-laptop-the-ui-loaded-uploads-succeeded-apis-responded-4jcj</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/mrzaizai2k" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" alt="mrzaizai2k"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/mrzaizai2k/til-cors-ips-and-the-day-my-demo-crashed-45ce" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;TIL: CORS, IPs, and the Day My Demo Crashed 🔒&lt;/h2&gt;
      &lt;h3&gt;Mai Chi Bao ・ Sep 29 '25&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#mrzaizai2k&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#vscode&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#devops&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>webdev</category>
      <category>mrzaizai2k</category>
      <category>vscode</category>
      <category>devops</category>
    </item>
    <item>
      <title>TIL: CORS, IPs, and the Day My Demo Crashed 🔒</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Mon, 29 Sep 2025 12:00:00 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/til-cors-ips-and-the-day-my-demo-crashed-45ce</link>
      <guid>https://dev.to/mrzaizai2k/til-cors-ips-and-the-day-my-demo-crashed-45ce</guid>
      <description>&lt;p&gt;Everything worked perfectly on my laptop. 🚀 The UI loaded, uploads succeeded, APIs responded instantly.&lt;br&gt;&lt;br&gt;
But on demo day, in front of my boss, everything collapsed. 💥  &lt;/p&gt;

&lt;p&gt;This post is a breakdown of how &lt;strong&gt;VS Code Remote-SSH port forwarding tricked me&lt;/strong&gt;, how I fixed it, and what I learned.&lt;br&gt;&lt;br&gt;
If you’ve ever wondered why your app works fine locally but fails when someone else tries — this story might save you some embarrassment.  &lt;/p&gt;


&lt;h2&gt;
  
  
  Key Takeaways 📝
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why &lt;code&gt;localhost&lt;/code&gt; tricks you in Remote-SSH and how VS Code port forwarding can hide real network issues.
&lt;/li&gt;
&lt;li&gt;How to correctly configure backend (&lt;code&gt;0.0.0.0&lt;/code&gt;), frontend (server IP/hostname), and CORS so others can access your app.
&lt;/li&gt;
&lt;li&gt;Why testing in a clean environment (without port forwarding) is essential before demo day.
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
⚡ The Setup and Demo Day Disaster
&lt;/li&gt;
&lt;li&gt;
🔍 Chasing the Problem
&lt;/li&gt;
&lt;li&gt;
🎭 VS Code Port Forwarding: The Hidden Culprit
&lt;/li&gt;
&lt;li&gt;
🛠️ The Fix
&lt;/li&gt;
&lt;li&gt;
🔄 The CORS Twist
&lt;/li&gt;
&lt;li&gt;
✅ Conclusion
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  The Setup and Demo Day Disaster ⚡
&lt;/h2&gt;

&lt;p&gt;In my frontend &lt;code&gt;.env&lt;/code&gt; I had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REACT_APP_API_BASE=http://localhost:6489
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BACKEND_PORT=6489
BACKEND_HOST=localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my laptop (via &lt;strong&gt;VS Code Remote-SSH&lt;/strong&gt;) everything worked. UI loaded, uploads went through, API calls succeeded.&lt;/p&gt;

&lt;p&gt;But demo day came. My boss opened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://10.X.X.X:6485
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend loaded, but every upload and API call failed.&lt;br&gt;
I was lost — &lt;em&gt;why does it only work on my machine, but not his?&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Chasing the Problem 🔍
&lt;/h2&gt;

&lt;p&gt;At first, I thought the backend had crashed. Logs were fine.&lt;br&gt;
Then I suspected CORS. No issues there either.&lt;/p&gt;

&lt;p&gt;Finally, I asked &lt;strong&gt;ChatGPT&lt;/strong&gt;. It suggested:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Change backend host to &lt;code&gt;0.0.0.0&lt;/code&gt; and frontend API base to server IP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tried it — and it worked. 🎉&lt;br&gt;
But I still didn’t understand why it only worked for me before.&lt;/p&gt;


&lt;h2&gt;
  
  
  VS Code Port Forwarding: The Hidden Culprit 🎭
&lt;/h2&gt;

&lt;p&gt;Digging deeper, I discovered the real cause:&lt;br&gt;
&lt;strong&gt;VS Code Remote-SSH was auto-forwarding my ports.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On my laptop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;localhost:6485&lt;/code&gt; → tunneled to frontend on the server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;localhost:6489&lt;/code&gt; → tunneled to backend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So for me, everything looked fine. But for my boss, &lt;code&gt;localhost&lt;/code&gt; pointed to his own machine — which had no backend at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual proof of the trick:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before (VS Code auto port forwarding): You now can access via localhost:6485 and 10.X.X.X:6485&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpbwvjmwve3c45go2om3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpbwvjmwve3c45go2om3.png" alt=" " width="571" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deleting ports → no more fake &lt;code&gt;localhost&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;Now only server IP works:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupg98n11rx2239314v1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupg98n11rx2239314v1j.png" alt=" " width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Note on VS Code Port Forwarding&lt;/strong&gt;&lt;br&gt;
If you run two VS Code Remote-SSH sessions for different projects, and both try to use the same port, VS Code will automatically increment the port number.&lt;/p&gt;

&lt;p&gt;That’s why sometimes when you open &lt;code&gt;http://localhost:6485&lt;/code&gt;, it gets auto-routed to &lt;code&gt;http://localhost:6486&lt;/code&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  Why Auto Port Forwarding Matters 🔑
&lt;/h3&gt;

&lt;p&gt;When I added authentication, using the server IP broke Microsoft login — it only accepts &lt;code&gt;localhost&lt;/code&gt; or HTTPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error when opening via server IP (&lt;code&gt;10.X.X.X:6485&lt;/code&gt;) without port forwarding:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wkx51b7j1v9d0j9iaa6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wkx51b7j1v9d0j9iaa6.png" alt=" " width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; forward ports in VS Code (or manually with SSH):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-L&lt;/span&gt; 6485:localhost:6485 &lt;span class="nt"&gt;-L&lt;/span&gt; 6489:localhost:6489 user@10.X.X.X
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now login works again on &lt;code&gt;http://localhost:6485&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ig28edpxppc90gqcrh2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ig28edpxppc90gqcrh2.png" alt=" " width="794" height="595"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;But there’s a catch:&lt;/strong&gt;&lt;br&gt;
This solution is fine for local development, but not practical for demos — you can’t expect customers to forward ports.&lt;/p&gt;

&lt;p&gt;If your app relies on Microsoft login (or similar auth), you should set up &lt;strong&gt;HTTPS on the server&lt;/strong&gt;.&lt;br&gt;
That way, customers can simply access it via:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://10.X.X.X:6485
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But after that, new CORS problems surfaced — which leads to the next lesson.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix 🛠️
&lt;/h2&gt;

&lt;p&gt;I fixed configs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BACKEND_PORT=6489
BACKEND_HOST=0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REACT_APP_API_BASE=http://10.X.X.X:6489
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;localhost&lt;/code&gt; (127.0.0.1)&lt;/strong&gt;&lt;br&gt;
Binding your backend to &lt;code&gt;localhost&lt;/code&gt; means it only accepts connections from the same machine it’s running on.&lt;br&gt;
In other words, the server is listening only to itself. Anyone trying to reach it from another device on the network won’t be able to connect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;0.0.0.0&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Binding to &lt;code&gt;0.0.0.0&lt;/code&gt; tells the backend to listen on &lt;strong&gt;all available network interfaces&lt;/strong&gt; (LAN, Wi-Fi, etc.).&lt;br&gt;
This makes your service accessible not just from the local machine, but also from other devices on the same network — such as your boss’s laptop during the demo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Frontend API calls&lt;/strong&gt;&lt;br&gt;
Since the frontend runs in your user’s browser, it can’t resolve your &lt;code&gt;localhost&lt;/code&gt; (that points to &lt;em&gt;their&lt;/em&gt; machine, not your server).&lt;br&gt;
The frontend must call the backend using the &lt;strong&gt;server’s IP address or hostname&lt;/strong&gt; (e.g., &lt;code&gt;http://10.X.X.X:6489&lt;/code&gt;), so that it correctly points to the server where the backend is actually running.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The CORS Twist 🔄
&lt;/h2&gt;

&lt;p&gt;Later, I caught another mistake.&lt;br&gt;
My backend allowed only &lt;code&gt;SERVER_IP&lt;/code&gt;, but when the frontend ran on a &lt;strong&gt;different machine&lt;/strong&gt; (&lt;code&gt;FRONT_END_IP&lt;/code&gt;), the requests were blocked.&lt;/p&gt;

&lt;p&gt;Fix was to add both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;FRONT_END_IP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;allowed_origins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;FRONT_END_IP&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;allowed_origins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;FRONT_END_IP&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had lazily assumed &lt;code&gt;SERVER_IP == FRONT_END_IP&lt;/code&gt;. Wrong — and it silently broke requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion ✅
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;localhost&lt;/code&gt; ≠ server IP.&lt;/li&gt;
&lt;li&gt;Remote-SSH port forwarding can &lt;strong&gt;trick you&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Always bind backend to &lt;code&gt;0.0.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Always point frontend to server IP or hostname.&lt;/li&gt;
&lt;li&gt;Align CORS with the actual machine hosting frontend.&lt;/li&gt;
&lt;li&gt;Test from a clean environment, not your tunneled setup.&lt;/li&gt;
&lt;li&gt;And before demo day → &lt;strong&gt;disable VS Code auto port-forwarding&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Future me: no more blind trust in &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>mrzaizai2k</category>
      <category>vscode</category>
      <category>devops</category>
    </item>
    <item>
      <title>No subscriptions, no paywalls — just a straightforward QR generator that works right in your browser 🚀.</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Tue, 16 Sep 2025 02:46:09 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/no-subscriptions-no-paywalls-just-a-straightforward-qr-generator-that-works-right-in-your-1npg</link>
      <guid>https://dev.to/mrzaizai2k/no-subscriptions-no-paywalls-just-a-straightforward-qr-generator-that-works-right-in-your-1npg</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-qr-generator-5e37" class="crayons-story__hidden-navigation-link"&gt;TIL: Building a Simple QR Generator&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mrzaizai2k" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" alt="mrzaizai2k profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mrzaizai2k" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mai Chi Bao
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mai Chi Bao
                
              
              &lt;div id="story-author-preview-content-2839292" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mrzaizai2k" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mai Chi Bao&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-qr-generator-5e37" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 15 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-qr-generator-5e37" id="article-link-2839292"&gt;
          TIL: Building a Simple QR Generator
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tooling"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tooling&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mrzaizai2k"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mrzaizai2k&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/css"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;css&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/html"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;html&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-qr-generator-5e37" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-qr-generator-5e37#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              2&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>tooling</category>
      <category>mrzaizai2k</category>
      <category>css</category>
      <category>html</category>
    </item>
    <item>
      <title>TIL: Building a Simple QR Generator</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Mon, 15 Sep 2025 12:49:00 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/til-building-a-simple-qr-generator-5e37</link>
      <guid>https://dev.to/mrzaizai2k/til-building-a-simple-qr-generator-5e37</guid>
      <description>&lt;p&gt;I built a &lt;strong&gt;lightweight, open-source tool&lt;/strong&gt; to create customized QR codes.&lt;br&gt;&lt;br&gt;
No subscriptions, no paywalls — just a straightforward QR generator that works right in your browser 🚀.&lt;/p&gt;

&lt;p&gt;👉 Try it here: &lt;a href="https://mrzaizai2k.github.io/qr_generator" rel="noopener noreferrer"&gt;Simple QR Generator&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📑 Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why I Built This&lt;/li&gt;
&lt;li&gt;Features&lt;/li&gt;
&lt;li&gt;How It Works&lt;/li&gt;
&lt;li&gt;Demo Screenshot&lt;/li&gt;
&lt;li&gt;Why I Like It&lt;/li&gt;
&lt;li&gt;Reference&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💭 Why I Built This
&lt;/h2&gt;

&lt;p&gt;Most QR code tools online feel &lt;strong&gt;bloated or restrictive&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Many require &lt;strong&gt;accounts or subscriptions&lt;/strong&gt; just to download a QR code.
&lt;/li&gt;
&lt;li&gt;Some lock basic features like color, logo, or export behind a &lt;strong&gt;paywall&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Others are overloaded with ads, making them slow and frustrating.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I just wanted something &lt;strong&gt;clean, simple, and free&lt;/strong&gt; — a tool where you can generate and customize QR codes without hassle.&lt;br&gt;&lt;br&gt;
That’s why I created &lt;strong&gt;Simple QR Generator&lt;/strong&gt; — a project that anyone can use instantly in their browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Generate QR codes from &lt;strong&gt;any text or URL&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Customize:

&lt;ul&gt;
&lt;li&gt;Colors&lt;/li&gt;
&lt;li&gt;Shapes&lt;/li&gt;
&lt;li&gt;Styles&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Add a &lt;strong&gt;logo&lt;/strong&gt; (upload your own or use an image URL).&lt;/li&gt;

&lt;li&gt;Adjust:

&lt;ul&gt;
&lt;li&gt;Size&lt;/li&gt;
&lt;li&gt;Margin&lt;/li&gt;
&lt;li&gt;Error correction level&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Export to &lt;strong&gt;PNG&lt;/strong&gt; or &lt;strong&gt;SVG&lt;/strong&gt;.&lt;/li&gt;

&lt;li&gt;Copy QR codes directly to clipboard.&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚡ How It Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Enter your text or URL in the input box.
&lt;/li&gt;
&lt;li&gt;Adjust customization options (color, style, size, error correction).
&lt;/li&gt;
&lt;li&gt;(Optional) Upload or link a logo to embed inside the QR code.
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Generate&lt;/strong&gt;, then either:

&lt;ul&gt;
&lt;li&gt;Export as &lt;strong&gt;PNG&lt;/strong&gt; or &lt;strong&gt;SVG&lt;/strong&gt;, or
&lt;/li&gt;
&lt;li&gt;Copy directly to clipboard for quick use.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of this happens &lt;strong&gt;entirely in your browser&lt;/strong&gt; — no server-side processing, no data collection.&lt;/p&gt;




&lt;h2&gt;
  
  
  🖥️ Demo Screenshot
&lt;/h2&gt;

&lt;p&gt;Here’s a quick look at the tool in action 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5zpmtoqhllk7lsgje9qn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5zpmtoqhllk7lsgje9qn.png" alt=" " width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can try it live here 👉 &lt;a href="https://mrzaizai2k.github.io/qr_generator" rel="noopener noreferrer"&gt;Simple QR Generator&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Why I Like It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight&lt;/strong&gt;: works instantly in any browser.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No limits&lt;/strong&gt;: all features are free and open-source.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable&lt;/strong&gt;: from logos to colors, you can make QR codes match your style.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical&lt;/strong&gt;: whether it’s sharing a link, business info, or Wi-Fi credentials, it just works.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It started as a small side project for myself, but it turned into a neat tool worth sharing.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 Reference
&lt;/h2&gt;

&lt;p&gt;All the code is open-source on my GitHub:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/mrzaizai2k/simple_qr_generator" rel="noopener noreferrer"&gt;mrzaizai2k/simple_qr_generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to clone it, star it ⭐, or suggest improvements.  &lt;/p&gt;

</description>
      <category>tooling</category>
      <category>mrzaizai2k</category>
      <category>css</category>
      <category>html</category>
    </item>
    <item>
      <title>Hi do you guys want to test?</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Thu, 11 Sep 2025 14:43:24 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/hi-do-you-guys-want-to-test-49og</link>
      <guid>https://dev.to/mrzaizai2k/hi-do-you-guys-want-to-test-49og</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-typing-practice-tool-33h8" class="crayons-story__hidden-navigation-link"&gt;TIL: Building a Simple Typing Practice Tool&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mrzaizai2k" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" alt="mrzaizai2k profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mrzaizai2k" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mai Chi Bao
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mai Chi Bao
                
              
              &lt;div id="story-author-preview-content-2821601" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mrzaizai2k" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mai Chi Bao&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-typing-practice-tool-33h8" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 8 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-typing-practice-tool-33h8" id="article-link-2821601"&gt;
          TIL: Building a Simple Typing Practice Tool
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tooling"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tooling&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mrzaizai2k"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mrzaizai2k&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/html"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;html&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/css"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;css&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-typing-practice-tool-33h8" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/mrzaizai2k/til-building-a-simple-typing-practice-tool-33h8#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              1&lt;span class="hidden s:inline"&gt; comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>tooling</category>
      <category>mrzaizai2k</category>
      <category>html</category>
      <category>css</category>
    </item>
    <item>
      <title>TIL: Building a Simple Typing Practice Tool</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Mon, 08 Sep 2025 13:05:00 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/til-building-a-simple-typing-practice-tool-33h8</link>
      <guid>https://dev.to/mrzaizai2k/til-building-a-simple-typing-practice-tool-33h8</guid>
      <description>&lt;h1&gt;
  
  
  📝 Today I Learned: Building a Simple Typing Practice Tool
&lt;/h1&gt;

&lt;p&gt;I built a &lt;strong&gt;really small web-based tool&lt;/strong&gt; to enhance typing skills with real-time feedback.&lt;br&gt;&lt;br&gt;
It’s just a single HTML file — no backend, no frameworks, no setup. You open it in your browser and start typing 🚀.&lt;/p&gt;




&lt;h2&gt;
  
  
  📑 Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why I Built This&lt;/li&gt;
&lt;li&gt;Features&lt;/li&gt;
&lt;li&gt;How It Works&lt;/li&gt;
&lt;li&gt;Demo Screenshot&lt;/li&gt;
&lt;li&gt;Why I Like It&lt;/li&gt;
&lt;li&gt;Reference&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💭 Why I Built This
&lt;/h2&gt;

&lt;p&gt;Most typing practice tools I tried don’t &lt;strong&gt;support Vietnamese text well&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Their UI looks broken or ugly when practicing with Vietnamese.
&lt;/li&gt;
&lt;li&gt;They rarely offer fresh, real-world Vietnamese content.
&lt;/li&gt;
&lt;li&gt;I wanted to &lt;strong&gt;auto-generate practice text&lt;/strong&gt; from news so I don’t get bored.
&lt;/li&gt;
&lt;li&gt;I also wanted to &lt;strong&gt;customize the metrics&lt;/strong&gt; I care about (e.g., word accuracy, correct WPM).
&lt;/li&gt;
&lt;li&gt;Plus, having a &lt;strong&gt;dark mode toggle&lt;/strong&gt; and a cleaner UI makes practice more enjoyable.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s why I created this simple tool — &lt;strong&gt;just for myself at first&lt;/strong&gt; — but it turned out useful enough to share.  &lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Works everywhere — pure HTML, CSS, and JS.&lt;/li&gt;
&lt;li&gt;Auto-fetches &lt;strong&gt;Vietnamese 🇻🇳&lt;/strong&gt; or &lt;strong&gt;English 🇺🇸&lt;/strong&gt; news text for practice.&lt;/li&gt;
&lt;li&gt;Customizable reference text (you can paste or write your own).&lt;/li&gt;
&lt;li&gt;Tracks:

&lt;ul&gt;
&lt;li&gt;Time (seconds)&lt;/li&gt;
&lt;li&gt;Character Accuracy (%)&lt;/li&gt;
&lt;li&gt;Word Accuracy (%)&lt;/li&gt;
&lt;li&gt;Words Per Minute (WPM)&lt;/li&gt;
&lt;li&gt;Correct WPM&lt;/li&gt;
&lt;li&gt;Progress (typed vs total characters)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Real-time color coding:
✅ correct, ❌ incorrect, and 🎯 current character.
&lt;/li&gt;

&lt;li&gt;Auto-scrolls while typing.&lt;/li&gt;

&lt;li&gt;Completion message when done.&lt;/li&gt;

&lt;li&gt;Dark mode + Reset button.&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚡ How It Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You type directly on the displayed reference text.
&lt;/li&gt;
&lt;li&gt;Each keystroke updates:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Character accuracy&lt;/strong&gt; → (correct chars ÷ total typed).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Word accuracy&lt;/strong&gt; → (correct full words ÷ attempted words).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WPM&lt;/strong&gt; → counts total words per minute (including mistakes).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct WPM&lt;/strong&gt; → only correct words per minute.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;progress bar&lt;/strong&gt; and completion message keep you motivated.
&lt;/li&gt;
&lt;li&gt;You can fetch real news text from &lt;a href="https://vnexpress.net" rel="noopener noreferrer"&gt;VnExpress&lt;/a&gt; and practice with something fresh.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🖥️ Demo Screenshot
&lt;/h2&gt;

&lt;p&gt;You can try my &lt;a href="https://mrzaizai2k.github.io/typing" rel="noopener noreferrer"&gt;Simple Typing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm6elsjy1uikcmd1xo0j9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm6elsjy1uikcmd1xo0j9.png" alt=" " width="800" height="665"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also visit my repo here 👉 &lt;a href="https://github.com/mrzaizai2k/simple-typing/" rel="noopener noreferrer"&gt;mrzaizai2k/simple-typing&lt;/a&gt;.  &lt;/p&gt;

&lt;p&gt;Just download the &lt;code&gt;typing.html&lt;/code&gt; file, open it in any browser — and you’re ready to practice 🚀.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Why I Like It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Minimalistic: no dependencies, just one &lt;code&gt;.html&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Real-time feedback keeps me aware of typing mistakes instantly.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;news fetch feature&lt;/strong&gt; means I can always practice with new content, instead of boring lorem ipsum.&lt;/li&gt;
&lt;li&gt;Great for improving not just speed, but also &lt;strong&gt;accuracy&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔗 Reference
&lt;/h2&gt;

&lt;p&gt;All the code is live here on my GitHub:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/mrzaizai2k/simple-typing/" rel="noopener noreferrer"&gt;mrzaizai2k/simple-typing&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;Feel free to clone it, star it ⭐, or ask me anything.&lt;/p&gt;

</description>
      <category>tooling</category>
      <category>mrzaizai2k</category>
      <category>html</category>
      <category>css</category>
    </item>
    <item>
      <title>A few weeks ago, I built a Docker image for my personal portfolio project (a React app). Everything worked fine — until I checked the size... 💣 1.5 GB! For a static frontend app?! I realized this could slow down deployment, consume more storage, and mak</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Sun, 27 Jul 2025 03:34:52 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/a-few-weeks-ago-i-built-a-docker-image-for-my-personal-portfolio-project-a-react-app-everything-481d</link>
      <guid>https://dev.to/mrzaizai2k/a-few-weeks-ago-i-built-a-docker-image-for-my-personal-portfolio-project-a-react-app-everything-481d</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mrzaizai2k/from-15gb-to-200mb-how-i-slimmed-down-my-docker-image-like-a-pro-12em" class="crayons-story__hidden-navigation-link"&gt;From 1.5GB to 200MB: How I Slimmed Down My Docker Image Like a Pro 🚀&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mrzaizai2k" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" alt="mrzaizai2k profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mrzaizai2k" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mai Chi Bao
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mai Chi Bao
                
              
              &lt;div id="story-author-preview-content-2686123" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mrzaizai2k" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mai Chi Bao&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mrzaizai2k/from-15gb-to-200mb-how-i-slimmed-down-my-docker-image-like-a-pro-12em" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 21 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mrzaizai2k/from-15gb-to-200mb-how-i-slimmed-down-my-docker-image-like-a-pro-12em" id="article-link-2686123"&gt;
          From 1.5GB to 200MB: How I Slimmed Down My Docker Image Like a Pro 🚀
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mrzaizai2k"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mrzaizai2k&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/docker"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;docker&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/mrzaizai2k/from-15gb-to-200mb-how-i-slimmed-down-my-docker-image-like-a-pro-12em" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;12&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/mrzaizai2k/from-15gb-to-200mb-how-i-slimmed-down-my-docker-image-like-a-pro-12em#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              1&lt;span class="hidden s:inline"&gt; comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>mrzaizai2k</category>
      <category>webdev</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>From 1.5GB to 200MB: How I Slimmed Down My Docker Image Like a Pro 🚀</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Mon, 21 Jul 2025 12:26:00 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/from-15gb-to-200mb-how-i-slimmed-down-my-docker-image-like-a-pro-12em</link>
      <guid>https://dev.to/mrzaizai2k/from-15gb-to-200mb-how-i-slimmed-down-my-docker-image-like-a-pro-12em</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A real journey of "less is more" in Docker 🐳&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📚 Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎯 The Problem: Bloated Docker Image&lt;/li&gt;
&lt;li&gt;🔍 The Investigation&lt;/li&gt;
&lt;li&gt;
🧠 The Solution Explained

&lt;ul&gt;
&lt;li&gt;1. Add &lt;code&gt;.dockerignore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;2. Use Multi stage Builds&lt;/li&gt;
&lt;li&gt;3. Choose Lighter Base Images&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;📊 Before vs After: What Changed?&lt;/li&gt;

&lt;li&gt;🎁 Conclusion&lt;/li&gt;

&lt;li&gt;🔗 Reference&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎯 The Problem: Bloated Docker Image
&lt;/h2&gt;

&lt;p&gt;A few weeks ago, I built a Docker image for my personal portfolio project (a React app). Everything worked fine — until I checked the size...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💣 &lt;strong&gt;1.5 GB! For a static frontend app?!&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I realized this could slow down deployment, consume more storage, and make CI/CD pipelines sluggish. I had to fix this.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 The Investigation
&lt;/h2&gt;

&lt;p&gt;When I looked into my Docker setup, I noticed some rookie mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I didn’t have a &lt;code&gt;.dockerignore&lt;/code&gt; file 😅&lt;/li&gt;
&lt;li&gt;I was using only a single-stage build&lt;/li&gt;
&lt;li&gt;The base image was &lt;code&gt;node:18-alpine&lt;/code&gt;, which seemed fine but not optimal for production delivery&lt;/li&gt;
&lt;li&gt;I was copying everything (including &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, and build files) into the image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is my previous Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;  &lt;span class="c"&gt;# Use official Node image&lt;/span&gt;
  FROM node:18-alpine
  &lt;span class="c"&gt;# Set working directory&lt;/span&gt;
  WORKDIR /app
  &lt;span class="c"&gt;# Copy dependency files first (for better Docker caching)&lt;/span&gt;
  COPY package.json package-lock.json ./
  # Install dependencies
  RUN npm ci
  &lt;span class="c"&gt;# Copy rest of the application&lt;/span&gt;
  COPY . .
  # Build the React app
  RUN npm run build
  &lt;span class="c"&gt;# Install a lightweight static server&lt;/span&gt;
  RUN npm install -g serve 
  &lt;span class="c"&gt;# Expose port&lt;/span&gt;
  EXPOSE 3000
  &lt;span class="c"&gt;# Serve the static build&lt;/span&gt;
  CMD ["serve", "-s", "build", "-l", "3000"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was time to clean house.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 The Solution Explained
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Add dockerignore
&lt;/h3&gt;

&lt;p&gt;Think of &lt;code&gt;.dockerignore&lt;/code&gt; as &lt;code&gt;.gitignore&lt;/code&gt; for Docker. Without it, Docker copies &lt;em&gt;everything&lt;/em&gt; from your working directory — including huge folders like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node_modules/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.git/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*.md&lt;/code&gt; docs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added this &lt;code&gt;.dockerignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  node_modules/
  npm-debug.log
  .git
  .gitignore
  Dockerfile
  docker-compose.yml
  build/
  dist/
  .env
  *.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 &lt;strong&gt;Result&lt;/strong&gt;: Way less junk copied into the image = faster build time + smaller size&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Use Multi stage Builds
&lt;/h3&gt;

&lt;p&gt;Instead of one bloated Dockerfile, I split it into two stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Builder Stage&lt;/strong&gt;: Installs dependencies and runs the build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Stage&lt;/strong&gt;: Copies only what’s needed to serve the app
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;  &lt;span class="c"&gt;# Stage 1: Build&lt;/span&gt;
  FROM node:18-slim AS builder
  WORKDIR /app
  COPY package*.json ./
  RUN npm ci --only=production
  COPY . .
  RUN npm run build

  &lt;span class="c"&gt;# Stage 2: Serve&lt;/span&gt;
  FROM node:18-slim
  WORKDIR /app
  COPY --from=builder /app/build ./build
  RUN npm install -g serve
  EXPOSE 3000
  CMD ["serve", "-s", "build", "-l", "3000"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 &lt;strong&gt;Result&lt;/strong&gt;: No dev dependencies or source code in the final image. Just the build output. Lean &amp;amp; clean.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Choose Lighter Base Images
&lt;/h3&gt;

&lt;p&gt;Originally I used &lt;code&gt;node:18-alpine&lt;/code&gt;. It’s small but sometimes causes compatibility issues with native modules.&lt;/p&gt;

&lt;p&gt;I switched to &lt;code&gt;node:18-slim&lt;/code&gt;, which is slightly larger than Alpine but more stable for multi-stage builds and still much smaller than the full Node image.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Result&lt;/strong&gt;: Better balance between size and reliability.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Before vs After: What Changed?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📦 Image Size&lt;/td&gt;
&lt;td&gt;~1.5 GB&lt;/td&gt;
&lt;td&gt;~200 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚙️ Build Strategy&lt;/td&gt;
&lt;td&gt;Single-stage&lt;/td&gt;
&lt;td&gt;Multi-stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📁 Base Image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node:18-alpine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node:18-slim&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚫 .dockerignore&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;✅ Included to skip unneeded files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔁 Copied Files&lt;/td&gt;
&lt;td&gt;Everything (including &lt;code&gt;node_modules&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;build/&lt;/code&gt; output copied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚀 Performance&lt;/td&gt;
&lt;td&gt;Slow build + large deploy&lt;/td&gt;
&lt;td&gt;Faster build + minimal production image&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🎁 Conclusion
&lt;/h2&gt;

&lt;p&gt;If you apply the same strategies, you’ll get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Smaller Docker Images&lt;/strong&gt; → Faster pulls/pushes, lower storage cost&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Faster Builds&lt;/strong&gt; → Especially in CI/CD pipelines&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Better Security&lt;/strong&gt; → No unnecessary source code or secrets inside image&lt;/li&gt;
&lt;li&gt;🌍 &lt;strong&gt;Portable &amp;amp; Lightweight Images&lt;/strong&gt; → Ideal for microservices, serverless, and edge deployments&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔗 Reference
&lt;/h2&gt;

&lt;p&gt;All the code is live here on my GitHub:&lt;br&gt;
👉 &lt;a href="https://github.com/mrzaizai2k/Portfolio" rel="noopener noreferrer"&gt;mrzaizai2k/Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to clone it, star it ⭐, or ask me anything.&lt;/p&gt;




&lt;p&gt;Thanks for reading!&lt;br&gt;
If this helped you, share it with a friend who might still be shipping 1GB containers 😄&lt;/p&gt;

</description>
      <category>mrzaizai2k</category>
      <category>webdev</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>I'm not a professional web developer — but even so, just by following these 5 simple steps, I managed to hit 95 on PageSpeed 🚀 But hey — don’t just skim this. The last tip could make or break all your efforts. Stick around until the end for the real gam</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Tue, 15 Jul 2025 08:29:17 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/im-not-a-professional-web-developer-but-even-so-just-by-following-these-5-simple-steps-i-2ali</link>
      <guid>https://dev.to/mrzaizai2k/im-not-a-professional-web-developer-but-even-so-just-by-following-these-5-simple-steps-i-2ali</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mrzaizai2k/lets-be-honest-slow-websites-scare-people-away-34d4" class="crayons-story__hidden-navigation-link"&gt;Let’s Be Honest… Slow Websites Scare People Away 😢&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mrzaizai2k" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" alt="mrzaizai2k profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mrzaizai2k" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mai Chi Bao
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mai Chi Bao
                
              
              &lt;div id="story-author-preview-content-2662570" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mrzaizai2k" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1976523%2F358f751f-fd95-4b8b-becb-99bdb7b5faa9.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mai Chi Bao&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mrzaizai2k/lets-be-honest-slow-websites-scare-people-away-34d4" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 14 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mrzaizai2k/lets-be-honest-slow-websites-scare-people-away-34d4" id="article-link-2662570"&gt;
          Let’s Be Honest… Slow Websites Scare People Away 😢
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mrzaizai2k"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mrzaizai2k&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/performance"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;performance&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/mrzaizai2k/lets-be-honest-slow-websites-scare-people-away-34d4" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;7&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/mrzaizai2k/lets-be-honest-slow-websites-scare-people-away-34d4#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              2&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>react</category>
      <category>mrzaizai2k</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>Let’s Be Honest… Slow Websites Scare People Away 😢</title>
      <dc:creator>Mai Chi Bao</dc:creator>
      <pubDate>Mon, 14 Jul 2025 12:27:00 +0000</pubDate>
      <link>https://dev.to/mrzaizai2k/lets-be-honest-slow-websites-scare-people-away-34d4</link>
      <guid>https://dev.to/mrzaizai2k/lets-be-honest-slow-websites-scare-people-away-34d4</guid>
      <description>&lt;p&gt;I'm not a professional web developer — but even so, just by following these &lt;strong&gt;5 simple steps&lt;/strong&gt;, I managed to hit &lt;strong&gt;95 on PageSpeed&lt;/strong&gt; 🚀  &lt;/p&gt;

&lt;p&gt;But hey — don’t just skim this. The &lt;strong&gt;last tip&lt;/strong&gt; could make or break all your efforts.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Stick around until the end&lt;/strong&gt; for the real game-changer!&lt;/p&gt;
&lt;h2&gt;
  
  
  📚 Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔍 Analyze with PageSpeed&lt;/li&gt;
&lt;li&gt;✂️ Code Splitting and Reducing Unused Stuff&lt;/li&gt;
&lt;li&gt;🖼️ Optimize Images&lt;/li&gt;
&lt;li&gt;🔬 Code Review and Lazy Loading&lt;/li&gt;
&lt;li&gt;🎨 CSS Optimizations&lt;/li&gt;
&lt;li&gt;🌐 Real-World Network Testing&lt;/li&gt;
&lt;li&gt;🎁 What's Next&lt;/li&gt;
&lt;li&gt;📎 References&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  🔍 Analyze with PageSpeed
&lt;/h2&gt;

&lt;p&gt;Start with the easiest step — use &lt;a href="https://pagespeed.web.dev" rel="noopener noreferrer"&gt;PageSpeed Insights&lt;/a&gt; to check your site’s performance.&lt;br&gt;&lt;br&gt;
Here’s the result I got for my website &lt;a href="https://mrzaizai2k.xyz/" rel="noopener noreferrer"&gt;mrzaizai2k.xyz&lt;/a&gt;:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggdmk9157h7z4012iaz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggdmk9157h7z4012iaz6.png" alt="PageSpeed Score" width="800" height="678"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It not only gives you a score — it shows you exactly what’s slowing your site down:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpugpmjq0a94dnpo1hwsd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpugpmjq0a94dnpo1hwsd.png" alt="Diagnostics" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Step 1: Done&lt;/strong&gt; — you now know where your site stands and what to fix!&lt;/p&gt;


&lt;h2&gt;
  
  
  ✂️ Code Splitting and Reducing Unused Stuff
&lt;/h2&gt;

&lt;p&gt;JavaScript can be a performance killer if not handled properly.&lt;br&gt;&lt;br&gt;
By default, browsers download and parse &lt;strong&gt;all JS&lt;/strong&gt; — even if parts aren’t needed right away.&lt;/p&gt;

&lt;p&gt;That’s why we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Split code&lt;/strong&gt; using tools like Webpack or dynamic imports.&lt;/li&gt;
&lt;li&gt;Remove &lt;strong&gt;unused code&lt;/strong&gt;, especially third-party scripts or unused CSS classes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduces the size of initial payloads, speeds up rendering, and improves the &lt;strong&gt;Time to Interactive&lt;/strong&gt; metric ⚡&lt;/p&gt;

&lt;p&gt;💡 Imagine JS as a buffet — don’t serve the whole menu if the user only asked for a sandwich.&lt;/p&gt;


&lt;h2&gt;
  
  
  🖼️ Optimize Images
&lt;/h2&gt;

&lt;p&gt;Use tools like &lt;a href="https://tinypng.com/" rel="noopener noreferrer"&gt;TinyPNG&lt;/a&gt; to compress your images — no quality loss, just smaller size!  &lt;/p&gt;

&lt;p&gt;I optimized this image and saved &lt;strong&gt;68%&lt;/strong&gt; in size with just a few clicks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8zyypewmgkqp3jfkz860.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8zyypewmgkqp3jfkz860.png" alt="Optimized Example" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📸 Also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Crop&lt;/strong&gt; unnecessary parts.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;WebP&lt;/strong&gt; or &lt;strong&gt;AVIF&lt;/strong&gt; formats when possible.&lt;/li&gt;
&lt;li&gt;Set correct image dimensions to avoid layout shifts.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🔬 Code Review and Lazy Loading
&lt;/h2&gt;

&lt;p&gt;Here’s how I use &lt;code&gt;React.lazy()&lt;/code&gt; and &lt;code&gt;Suspense&lt;/code&gt; to load components &lt;strong&gt;only when they’re needed&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&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;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Home2&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;homeLogo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"home pic"&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"img-fluid"&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;450px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this works:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React.lazy&lt;/strong&gt;: breaks your app into smaller chunks. This means non-critical parts don’t load until needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;loading="lazy"&lt;/strong&gt; on images: tells the browser not to load off-screen images until the user scrolls to them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This greatly reduces initial load time, especially for users on low-end devices or slow networks 🌍&lt;/p&gt;




&lt;h2&gt;
  
  
  🎨 CSS Optimizations
&lt;/h2&gt;

&lt;p&gt;I keep my CSS &lt;strong&gt;minimal&lt;/strong&gt;, &lt;strong&gt;responsive&lt;/strong&gt;, and &lt;strong&gt;optimized for rendering&lt;/strong&gt;.&lt;br&gt;
Here’s one example from my code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;  &lt;span class="nc"&gt;.tech-icons&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4.5em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;89&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;168&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.137&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="m"&gt;0.4s&lt;/span&gt; &lt;span class="n"&gt;ease&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;h3&gt;
  
  
  What’s going on here:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🧽 &lt;strong&gt;Clean layout&lt;/strong&gt;: Simple margin/padding, avoiding deep nesting.&lt;/li&gt;
&lt;li&gt;🧠 &lt;strong&gt;Efficient rendering&lt;/strong&gt;: &lt;code&gt;transition&lt;/code&gt;, &lt;code&gt;box-shadow&lt;/code&gt;, and no heavy animations.&lt;/li&gt;
&lt;li&gt;📱 &lt;strong&gt;Responsive by default&lt;/strong&gt;: Paired with media queries to scale well on mobile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid bloated libraries and unnecessary CSS classes. Stick to what you use — nothing more.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌐 Real World Network Testing
&lt;/h2&gt;

&lt;p&gt;Your site might be fast for you, but what about users on 3G or rural networks?&lt;/p&gt;

&lt;p&gt;I tested my Projects page:&lt;br&gt;
🔗 &lt;a href="https://mrzaizai2k.xyz/project" rel="noopener noreferrer"&gt;mrzaizai2k.xyz/project&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looked fine on fast Wi-Fi:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feehdcecxu7ookuse4j7g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feehdcecxu7ookuse4j7g.png" alt="Normal Load" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But in &lt;strong&gt;Edge DevTools&lt;/strong&gt;, I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pressed &lt;code&gt;F12&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Opened the &lt;code&gt;Network&lt;/code&gt; tab&lt;/li&gt;
&lt;li&gt;Enabled &lt;strong&gt;Disable Cache&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Switched to &lt;strong&gt;Slow 3G&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What happened?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;YouTube previews didn’t load.&lt;/li&gt;
&lt;li&gt;No visual feedback = bad UX.&lt;/li&gt;
&lt;li&gt;User probably leaves before seeing your work.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  My Fix:
&lt;/h3&gt;

&lt;p&gt;I added a &lt;strong&gt;"Demo" button&lt;/strong&gt; as a fallback, so users can still explore projects even if previews fail to load 🔁&lt;/p&gt;

&lt;p&gt;Always think like a user:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"If I saw a blank screen, would I wait? Probably not."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🎁 What's Next
&lt;/h2&gt;

&lt;p&gt;🎯 Here’s the big one:&lt;br&gt;
Even with all the above, it’s &lt;strong&gt;not enough&lt;/strong&gt; without thinking about &lt;strong&gt;build and deployment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the next post, I’ll show you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to create a &lt;strong&gt;super-light Dockerfile&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;How &lt;code&gt;npm run build&lt;/code&gt; makes your app production-ready&lt;/li&gt;
&lt;li&gt;Why deploying static files the right way boosts speed like crazy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Don’t miss it — &lt;strong&gt;follow me&lt;/strong&gt; so you get notified when it’s live!&lt;br&gt;
👍 If this post helped you, please &lt;strong&gt;like&lt;/strong&gt;, &lt;strong&gt;comment&lt;/strong&gt;, or &lt;strong&gt;share&lt;/strong&gt;&lt;br&gt;
⭐️ A &lt;strong&gt;star&lt;/strong&gt; on the repo also means a lot!&lt;/p&gt;




&lt;h2&gt;
  
  
  📎 References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pagespeed.web.dev/analysis/https-mrzaizai2k-xyz/cz51h2jfy5?form_factor=desktop" rel="noopener noreferrer"&gt;PageSpeed Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💻 My Website: &lt;a href="https://mrzaizai2k.xyz" rel="noopener noreferrer"&gt;mrzaizai2k.xyz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📁 My Repo: &lt;a href="https://github.com/mrzaizai2k/Portfolio/tree/master" rel="noopener noreferrer"&gt;GitHub Portfolio&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>react</category>
      <category>mrzaizai2k</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
