<?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: HanoStudio</title>
    <description>The latest articles on DEV Community by HanoStudio (@hano).</description>
    <link>https://dev.to/hano</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3979732%2F2bdb80ba-7520-46ac-96d2-cf411fb33222.png</url>
      <title>DEV Community: HanoStudio</title>
      <link>https://dev.to/hano</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hano"/>
    <language>en</language>
    <item>
      <title>What I Learned Reading Unity's BuildReport After Building 16 APKs</title>
      <dc:creator>HanoStudio</dc:creator>
      <pubDate>Thu, 02 Jul 2026 05:13:51 +0000</pubDate>
      <link>https://dev.to/hano/what-i-learned-reading-unitys-buildreport-after-building-16-apks-4d7g</link>
      <guid>https://dev.to/hano/what-i-learned-reading-unitys-buildreport-after-building-16-apks-4d7g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Build Analyzer series, article 2.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/hanostudio/what-actually-makes-a-unity-build-so-large-3jh4"&gt;previous article&lt;/a&gt;, I built 16 Android APKs while changing one variable at a time. After that, I had the APK numbers. What I did not have yet was a good explanation for where those bytes came from.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I finished the build-size experiment, I almost published the wrong number.&lt;/p&gt;

&lt;p&gt;Unity handed me three different "build sizes," and for a moment I was not sure which one represented the file users actually download.&lt;/p&gt;

&lt;p&gt;That confusion is why I wrote a small &lt;code&gt;BuildReport&lt;/code&gt; parser. I already had the final APK deltas, but I wanted evidence from inside Unity's build output too.&lt;/p&gt;

&lt;p&gt;It helped, but not in the clean way I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first surprise: there was no single build size
&lt;/h2&gt;

&lt;p&gt;My first instinct was to look for "the build size" in Unity's &lt;code&gt;BuildReport&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That turned out to be the wrong mental model.&lt;/p&gt;

&lt;p&gt;In the final measured Android build, I had three different size numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Final APK file size&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.10 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BuildReport.summary.totalSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;143.54 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sum of &lt;code&gt;BuildReport.packedAssets&lt;/code&gt; entries&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.60 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At first glance that looks suspicious. How can the same build be 17 MiB, 143 MiB, and 5.6 MiB?&lt;/p&gt;

&lt;p&gt;The answer is that they are not measuring the same thing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APK bytes: the compressed artifact users download&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BuildReport.summary.totalSize&lt;/code&gt;: Unity's reported build output size&lt;/li&gt;
&lt;li&gt;Packed asset sum: serialized asset data inside packed build files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I separated those three, the rest of the experiment became much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest parser I needed
&lt;/h2&gt;

&lt;p&gt;I did not start by building a polished UI. I only wanted a repeatable text report after every build.&lt;/p&gt;

&lt;p&gt;Unity's &lt;code&gt;IPostprocessBuildWithReport&lt;/code&gt; was enough for that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEditor.Build&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEditor.Build.Reporting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BuildAssetSizeReporter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPostprocessBuildWithReport&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;callbackOrder&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnPostprocessBuild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildReport&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rankedAssets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CollectAssetSizes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;packedAssetBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rankedAssets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="m"&gt;0U&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;artifactBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetArtifactBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;LogSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rankedAssets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;packedAssetBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifactBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;WriteReports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rankedAssets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;packedAssetBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifactBytes&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;The full source is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hellohanostudio/BuildAnalyzer/blob/main/Assets/BuildAnalyzer/Editor/BuildAssetSizeReporter.cs" rel="noopener noreferrer"&gt;https://github.com/hellohanostudio/BuildAnalyzer/blob/main/Assets/BuildAnalyzer/Editor/BuildAssetSizeReporter.cs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important part is not the hook itself. It is what I decided to record:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The final APK file size from disk&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BuildReport.summary.totalSize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A ranked list of packed asset entries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted the report to make it hard for me to mix those up later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggregating packed assets
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BuildReport.packedAssets&lt;/code&gt; contains the packed files generated during the build. Each packed file contains &lt;code&gt;PackedAssetInfo&lt;/code&gt; entries.&lt;/p&gt;

&lt;p&gt;The fields I used were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sourceAssetPath&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sourceAssetGUID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packedSize&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing I had to handle: the same source asset can show up more than once. Unity is reporting packed entries, not a neat "one source file, one row" table.&lt;/p&gt;

&lt;p&gt;So I grouped by source path before sorting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;KeyValuePair&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ulong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;[]&lt;/span&gt; &lt;span class="nf"&gt;CollectAssetSizes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildReport&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sizesByPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ulong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;packedFile&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packedAssets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;packedFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sourceAssetPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;$"&amp;lt;generated:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sourceAssetGUID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;"&lt;/span&gt;
                &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sourceAssetPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;sizesByPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;currentSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;sizesByPath&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentSize&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packedSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sizesByPath&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&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;This was enough to check the asset-shaped parts of the experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the texture really shrink after ASTC?&lt;/li&gt;
&lt;li&gt;Did the audio data shrink after Vorbis?&lt;/li&gt;
&lt;li&gt;Did the Korean font subset remove real packed bytes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those questions, the parser was useful right away.&lt;/p&gt;

&lt;h2&gt;
  
  
  I still measured the APK directly
&lt;/h2&gt;

&lt;p&gt;I did not use &lt;code&gt;BuildReport.summary.totalSize&lt;/code&gt; as the APK size.&lt;/p&gt;

&lt;p&gt;For the artifact number, I read the file on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;ulong&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;GetArtifactBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputPath&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="kt"&gt;ulong&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produced a plain summary like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;artifact_bytes=17928357
artifact_mib=17.0978
build_report_total_bytes=150514033
build_report_total_mib=143.5414
packed_asset_bytes=5873204
packed_asset_mib=5.6011
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not fancy, but that is the point. I wanted boring measurement code because boring code is harder to argue with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CSV had a small surprise
&lt;/h2&gt;

&lt;p&gt;The parser also writes a ranked CSV. This is the top of &lt;code&gt;Results/raw/step-07-dotween-runtime-assets.csv&lt;/code&gt; from the final experiment step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rank,packed_bytes,packed_mib,source_asset_path
1,2796376,2.666832,"Built-in Texture2D: Splash Screen Unity Logo"
2,1871556,1.784855,"Assets/Experiment/Generated/ExperimentTexture.png"
3,656420,0.626011,"Resources/unity_builtin_extra"
4,477252,0.455143,"Assets/Experiment/Generated/ExperimentAudio.wav"
5,69510,0.066290,"Assets/Experiment/Generated/NotoSansKR-Regular.ttf"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The biggest packed texture was not even my generated test texture. It was Unity's splash-screen logo.&lt;/p&gt;

&lt;p&gt;That was a useful reminder. A ranked build report is not a list of "my files only". It includes built-in assets Unity packs into the player too.&lt;/p&gt;

&lt;p&gt;The raw reports are here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hellohanostudio/BuildAnalyzer/tree/main/Results/raw" rel="noopener noreferrer"&gt;https://github.com/hellohanostudio/BuildAnalyzer/tree/main/Results/raw&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The place where the parser failed
&lt;/h2&gt;

&lt;p&gt;The parser explained textures, audio, and fonts pretty well.&lt;/p&gt;

&lt;p&gt;Then I added DOTween.&lt;/p&gt;

&lt;p&gt;DOTween Free increased the APK by &lt;strong&gt;0.46 MiB&lt;/strong&gt;. But the packed-asset CSV attributed only &lt;strong&gt;192 bytes&lt;/strong&gt; to the DOTween DLL path.&lt;/p&gt;

&lt;p&gt;That was the moment the shape of the problem changed for me.&lt;/p&gt;

&lt;p&gt;The parser was not "wrong". It was just looking through one narrow window. DOTween affected generated code and metadata, so most of the growth appeared in files such as &lt;code&gt;libil2cpp.so&lt;/code&gt; and &lt;code&gt;global-metadata.dat&lt;/code&gt;, not in &lt;code&gt;BuildReport.packedAssets&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That was probably the most useful result of the whole parser exercise: it showed me exactly where the parser stopped being enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would use this for
&lt;/h2&gt;

&lt;p&gt;After this experiment, I would use this kind of parser for asset questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which texture is taking packed space?&lt;/li&gt;
&lt;li&gt;Did an importer setting change actually reduce build data?&lt;/li&gt;
&lt;li&gt;How much did a font contribute?&lt;/li&gt;
&lt;li&gt;Did one source asset appear through multiple packed entries?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would not use it alone for code-size questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much did a plugin increase IL2CPP output?&lt;/li&gt;
&lt;li&gt;Which assemblies affected native binary size?&lt;/li&gt;
&lt;li&gt;Did managed stripping change generated code size?&lt;/li&gt;
&lt;li&gt;Which files inside the APK changed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For that, I need to inspect the final artifact too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I kept
&lt;/h2&gt;

&lt;p&gt;The main thing I kept from this week is simple:&lt;/p&gt;

&lt;p&gt;Do not say "build size" as if it is one number.&lt;/p&gt;

&lt;p&gt;For every build, I now want these three values side by side:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Final artifact bytes from the file on disk&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BuildReport.summary.totalSize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Sum of &lt;code&gt;BuildReport.packedAssets[].contents[].packedSize&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then I use the packed-asset ranking as evidence, not as the final truth.&lt;/p&gt;

&lt;p&gt;That small separation prevented the biggest mistake I could have made in the experiment: confusing what Unity packed as assets with what users actually download.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Unity API: &lt;a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Build.Reporting.BuildReport-packedAssets.html" rel="noopener noreferrer"&gt;&lt;code&gt;BuildReport.packedAssets&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity API: &lt;a href="https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Build.Reporting.PackedAssetInfo.html" rel="noopener noreferrer"&gt;&lt;code&gt;PackedAssetInfo&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity API: &lt;a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Build.Reporting.BuildSummary-totalSize.html" rel="noopener noreferrer"&gt;&lt;code&gt;BuildSummary.totalSize&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Reproducible project: &lt;a href="https://github.com/hellohanostudio/BuildAnalyzer" rel="noopener noreferrer"&gt;https://github.com/hellohanostudio/BuildAnalyzer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Previous article: &lt;a href="https://dev.to/hanostudio/what-actually-makes-a-unity-build-so-large-3jh4"&gt;https://dev.to/hanostudio/what-actually-makes-a-unity-build-so-large-3jh4&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>csharp</category>
      <category>performance</category>
    </item>
    <item>
      <title>The app that concludes nothing until 14 days add up</title>
      <dc:creator>HanoStudio</dc:creator>
      <pubDate>Fri, 26 Jun 2026 15:50:02 +0000</pubDate>
      <link>https://dev.to/hano/the-app-that-concludes-nothing-until-14-days-add-up-32d6</link>
      <guid>https://dev.to/hano/the-app-that-concludes-nothing-until-14-days-add-up-32d6</guid>
      <description>&lt;p&gt;I built a mood tracker. But what I actually want to write about isn't "I shipped an app" — it's &lt;strong&gt;how carefully a product should speak when it barely has any data.&lt;/strong&gt; That's the question I sat with the longest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Mood Weather
&lt;/h2&gt;

&lt;p&gt;Most mood apps score your feelings. They turn a mood into a 1–10, draw a graph, and sometimes say things like "you're showing a tendency toward depression." I didn't want that. A day's mood feels less like a &lt;em&gt;verdict&lt;/em&gt; and more like a &lt;em&gt;passing state&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I made the metaphor weather. Today is clear, yesterday was fog. Instead of "I'm a wreck," I wanted you to be able to say "today's weather is stormy." This isn't a logging app — it's an app for &lt;strong&gt;observing, carefully.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest decision: the voice
&lt;/h2&gt;

&lt;p&gt;What I deliberated on longest wasn't a feature. It was a single sentence. &lt;strong&gt;Do I say "you are like this," or "here's a signal I'm seeing"?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The less data you have, the more a conclusion becomes a lie. Telling someone "you're sensitive to caffeine" off three days of logs is just false. So I pinned the app's identity to one line — &lt;em&gt;signal, not diagnosis.&lt;/em&gt; The copy, the screens, the stat cards all follow it. Even when it surfaces a connection, it shows the sample size as a plain number, like "3 of 4 days." It doesn't hide how small that is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10-second check-in problem
&lt;/h2&gt;

&lt;p&gt;My first input form was heavy: several sliders, number fields, a note. But in an app you use every day, friction is death. Users don't open it again on day two.&lt;/p&gt;

&lt;p&gt;So I made the default path "save just the weather." Context (sleep, movement, and so on) is added &lt;em&gt;only when you want to.&lt;/em&gt; On mobile, all six weathers fit at a glance and the save button is pinned to the bottom. And one line for the days you skip — &lt;em&gt;a missed day isn't a failure, it's a blank.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fitzpkc4u53k5jb7vn7d4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fitzpkc4u53k5jb7vn7d4.png" alt="Mood Weather check-in: a card-style view showing the 10-second weather check-in and the app screen" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The first 14-day wall
&lt;/h2&gt;

&lt;p&gt;Patterns only show up once data accumulates. An "insight" before 14 days is just noise dressed up as truth.&lt;/p&gt;

&lt;p&gt;So I locked the report until day 14. Instead of leaving that gap empty, I turned it into a sense of progress — &lt;code&gt;1/14&lt;/code&gt;, &lt;code&gt;2/14&lt;/code&gt; — and from day three, a light preview. Give a small reward, but defer the conclusions. I chose saying less over building hooks to keep people around.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3adr1dpn3uw1k5wgisj0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3adr1dpn3uw1k5wgisj0.png" alt="Mood Weather report: a card-style view showing repeated patterns without hard conclusions" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Local storage and backup
&lt;/h2&gt;

&lt;p&gt;No login. Open the app and you're in; records stay on this device only. For an emotional journal, privacy &lt;em&gt;is&lt;/em&gt; trust.&lt;/p&gt;

&lt;p&gt;Local-only has a weakness, though — data can vanish. So I added JSON export/import as a safety net. I also added optional outside-weather capture: the location is used once to fetch the weather, and the coordinates aren't stored. Trust is the whole point of this app, so I was especially careful with that one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going public
&lt;/h2&gt;

&lt;p&gt;It's on Vercel. Still an experiment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This app won't tell you who you are. It just shows you what your mind's weather has been like lately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Link: &lt;strong&gt;&lt;a href="https://mode-weather.vercel.app" rel="noopener noreferrer"&gt;https://mode-weather.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;That's the concept and the first deploy. I'm not trying to claim "this is the right answer." I made these decisions, and the real test starts when actual 14-day histories begin to accumulate. That result is for #2.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>indiehackers</category>
      <category>webdev</category>
      <category>pwa</category>
    </item>
    <item>
      <title>What Actually Makes a Unity Build So Large?</title>
      <dc:creator>HanoStudio</dc:creator>
      <pubDate>Sun, 21 Jun 2026 14:59:18 +0000</pubDate>
      <link>https://dev.to/hano/what-actually-makes-a-unity-build-so-large-3jh4</link>
      <guid>https://dev.to/hano/what-actually-makes-a-unity-build-so-large-3jh4</guid>
      <description>&lt;p&gt;Eight controlled Unity Android builds show how texture, audio, Korean font, and DOTween choices change APK size, with reproducible data and source code.&lt;/p&gt;

&lt;p&gt;Most developers know that textures are heavy and uncompressed audio is bad. But ask which setting added how many megabytes to a real APK, and the answer often gets vague.&lt;/p&gt;

&lt;p&gt;After 14 years of building and shipping Unity mobile games, I wanted measurements rather than folklore. I started with a minimal project, changed one variable at a time, built eight Android APKs, and then repeated the entire run.&lt;/p&gt;

&lt;p&gt;The largest difference between the two runs was &lt;strong&gt;8 bytes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For per-asset measurements, I used a small BuildReport parser, which I break down in the follow-up article: &lt;a href="https://dev.to/hano/what-i-learned-reading-unitys-buildreport-after-building-16-apks-4d7g"&gt;What I Learned Reading Unity's BuildReport After Building 16 APKs&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Run it yourself:&lt;/strong&gt; The complete Unity project, build automation, both repeat-run CSVs, and every per-step raw report are public in the &lt;a href="https://github.com/hellohanostudio/BuildAnalyzer" rel="noopener noreferrer"&gt;BuildAnalyzer GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Unity &lt;code&gt;6000.3.7f1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Android APK, not AAB&lt;/li&gt;
&lt;li&gt;IL2CPP, ARM64 only&lt;/li&gt;
&lt;li&gt;Built-in Render Pipeline&lt;/li&gt;
&lt;li&gt;Development Build off&lt;/li&gt;
&lt;li&gt;Managed Stripping Level: Medium&lt;/li&gt;
&lt;li&gt;Engine code stripping enabled&lt;/li&gt;
&lt;li&gt;One scene with one camera, one directional light, and one cube&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scene included a small reference harness so every test asset was genuinely referenced. That makes the &lt;strong&gt;14.32 MiB&lt;/strong&gt; baseline specific to this experiment, not a universal Unity minimum.&lt;/p&gt;

&lt;p&gt;I measured the APK itself with &lt;code&gt;FileInfo.Length&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;artifactBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because &lt;code&gt;BuildReport.summary.totalSize&lt;/code&gt;, the sum of packed assets, and the final compressed APK size are three different metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;Each row changes one condition from the row immediately before it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Controlled change&lt;/th&gt;
&lt;th&gt;APK size&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Minimal experiment project&lt;/td&gt;
&lt;td&gt;14.32 MiB&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Referenced 2048x2048 texture, Android RGBA32&lt;/td&gt;
&lt;td&gt;28.12 MiB&lt;/td&gt;
&lt;td&gt;+13.80 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Same texture, Android ASTC 6x6&lt;/td&gt;
&lt;td&gt;16.14 MiB&lt;/td&gt;
&lt;td&gt;-11.98 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Add referenced 30-second stereo audio, PCM&lt;/td&gt;
&lt;td&gt;21.19 MiB&lt;/td&gt;
&lt;td&gt;+5.05 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Same audio, Vorbis quality 0.5&lt;/td&gt;
&lt;td&gt;16.60 MiB&lt;/td&gt;
&lt;td&gt;-4.59 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Add full Noto Sans KR dynamic font&lt;/td&gt;
&lt;td&gt;19.49 MiB&lt;/td&gt;
&lt;td&gt;+2.89 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Same font, app-specific subset&lt;/td&gt;
&lt;td&gt;16.63 MiB&lt;/td&gt;
&lt;td&gt;-2.86 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Add DOTween, preserved and referenced at runtime&lt;/td&gt;
&lt;td&gt;17.10 MiB&lt;/td&gt;
&lt;td&gt;+0.46 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4n12f0fchyzqvyc28fic.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4n12f0fchyzqvyc28fic.png" alt="Bar chart of APK size after all eight controlled changes" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Texture: RGBA32 vs ASTC
&lt;/h2&gt;

&lt;p&gt;The test texture was an opaque, deterministic 2048x2048 noise image with mipmaps disabled. Using high-entropy pixels prevented APK ZIP compression from making an uncompressed texture look artificially cheap.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Android format&lt;/th&gt;
&lt;th&gt;Packed texture&lt;/th&gt;
&lt;th&gt;APK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RGBA32&lt;/td&gt;
&lt;td&gt;16.00 MiB&lt;/td&gt;
&lt;td&gt;28.12 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASTC 6x6&lt;/td&gt;
&lt;td&gt;1.78 MiB&lt;/td&gt;
&lt;td&gt;16.14 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ASTC reduced the texture's packed contribution by &lt;strong&gt;88.84%&lt;/strong&gt; and removed &lt;strong&gt;11.98 MiB&lt;/strong&gt; from the APK. The ASTC build was still 1.82 MiB above baseline; compression did not make the texture free.&lt;/p&gt;

&lt;p&gt;The practical lesson is not simply "use ASTC." Pick a format and block size that your target devices support, then inspect the visual cost on real hardware. But leaving a large mobile texture as RGBA32 is an expensive default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio: PCM vs Vorbis
&lt;/h2&gt;

&lt;p&gt;The audio input was deterministic 30-second stereo PCM at 44.1 kHz. It contained several tones plus noise so it behaved more like real content than a silent WAV.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Android format&lt;/th&gt;
&lt;th&gt;Packed audio&lt;/th&gt;
&lt;th&gt;APK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PCM&lt;/td&gt;
&lt;td&gt;5.05 MiB&lt;/td&gt;
&lt;td&gt;21.19 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vorbis, quality 0.5&lt;/td&gt;
&lt;td&gt;0.46 MiB&lt;/td&gt;
&lt;td&gt;16.60 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Vorbis reduced the packed audio by &lt;strong&gt;90.98%&lt;/strong&gt; and removed &lt;strong&gt;4.59 MiB&lt;/strong&gt; from the APK.&lt;/p&gt;

&lt;p&gt;That does not make Vorbis correct for every clip. Short, latency-sensitive effects and long music tracks have different runtime requirements. The important part is that import format is a measurable build-size decision, not a housekeeping detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Korean font: full source vs app subset
&lt;/h2&gt;

&lt;p&gt;For the font test I used the same Noto Sans KR Regular dynamic font in both builds. The first build embedded the full source font. The second used a subset containing the characters required by a small example UI plus ASCII letters, digits, and punctuation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Font condition&lt;/th&gt;
&lt;th&gt;Packed font&lt;/th&gt;
&lt;th&gt;APK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full Noto Sans KR&lt;/td&gt;
&lt;td&gt;5.94 MiB&lt;/td&gt;
&lt;td&gt;19.49 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App-specific subset&lt;/td&gt;
&lt;td&gt;0.07 MiB&lt;/td&gt;
&lt;td&gt;16.63 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Subsetting reduced the packed font data by &lt;strong&gt;98.88%&lt;/strong&gt; and removed &lt;strong&gt;2.86 MiB&lt;/strong&gt; from the APK.&lt;/p&gt;

&lt;p&gt;This is specifically a &lt;strong&gt;dynamic source-font&lt;/strong&gt; comparison. A TextMeshPro static atlas has different tradeoffs, and a dynamic font may need fallback glyphs or downloadable language packs. Do not quote this percentage for every font pipeline. Do measure the character coverage your product actually ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  DOTween: the part PackedAssets did not explain
&lt;/h2&gt;

&lt;p&gt;For the final step I enabled the DOTween Free DLL for Android, preserved it through linking, and referenced it at runtime through the experiment harness.&lt;/p&gt;

&lt;p&gt;The APK grew by &lt;strong&gt;0.46 MiB&lt;/strong&gt;. But &lt;code&gt;PackedAssetInfo&lt;/code&gt; attributed only &lt;strong&gt;192 bytes&lt;/strong&gt; to the DLL path.&lt;/p&gt;

&lt;p&gt;Most of the increase appeared elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;libil2cpp.so&lt;/code&gt; compressed size grew by about 0.36 MiB.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;global-metadata.dat&lt;/code&gt; compressed size grew by about 0.06 MiB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the result that changed how I think about the analyzer. A per-asset report is necessary, but it cannot explain all code growth. A useful build analyzer needs to show at least three layers separately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Final artifact bytes&lt;/li&gt;
&lt;li&gt;Packed asset contributions&lt;/li&gt;
&lt;li&gt;Native and managed code or metadata changes&lt;/li&gt;
&lt;/ol&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F64c8unjk3ra0tu277glf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F64c8unjk3ra0tu277glf.png" alt="Unity Editor evidence window showing measured APK, packed asset, importer, and repeatability data" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;The texture was the largest single increase, but the font result was more useful. A full Korean dynamic font added 2.89 MiB to this APK; an app-specific subset recovered almost all of it.&lt;/p&gt;

&lt;p&gt;The second surprise was the metric mismatch. In the final build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APK artifact: &lt;strong&gt;17.10 MiB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BuildReport.summary.totalSize&lt;/code&gt;: &lt;strong&gt;143.54 MiB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Packed asset sum: &lt;strong&gt;5.60 MiB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these values is wrong. They answer different questions. Reporting one as if it were another creates a very convincing but incorrect chart.&lt;/p&gt;

&lt;p&gt;Finally, the two full experiment runs were remarkably stable. Across 16 APK builds, each matching step differed by at most 8 bytes. That made the large deltas easy to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Build size is not one big mistake. It is an accumulation of import formats, referenced content, font coverage, enabled code, engine modules, and packaging.&lt;/p&gt;

&lt;p&gt;The controlled experiment produced three high-value changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ASTC 6x6: &lt;strong&gt;-11.98 MiB&lt;/strong&gt; versus RGBA32&lt;/li&gt;
&lt;li&gt;Vorbis quality 0.5: &lt;strong&gt;-4.59 MiB&lt;/strong&gt; versus PCM&lt;/li&gt;
&lt;li&gt;App-specific Korean font subset: &lt;strong&gt;-2.86 MiB&lt;/strong&gt; versus the full font&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact numbers belong to this project and configuration. The method is the reusable part: fix the build settings, change one variable, measure the final artifact, and use BuildReport to explain what changed without mistaking it for the artifact itself.&lt;/p&gt;

&lt;p&gt;You can clone the &lt;a href="https://github.com/hellohanostudio/BuildAnalyzer" rel="noopener noreferrer"&gt;public experiment repository&lt;/a&gt; and run all eight builds from the Unity menu or command line.&lt;/p&gt;

&lt;p&gt;Next, I will apply the same measurement loop to a real mobile project and track which changes survive contact with production assets.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a Unity client developer with 14 years of experience in mobile and live-service games. I write about optimization problems I keep encountering in production.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment files
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/hellohanostudio/BuildAnalyzer" rel="noopener noreferrer"&gt;hellohanostudio/BuildAnalyzer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Automation: &lt;a href="https://github.com/hellohanostudio/BuildAnalyzer/blob/main/Assets/BuildAnalyzer/Editor/ExperimentBuildRunner.cs" rel="noopener noreferrer"&gt;&lt;code&gt;ExperimentBuildRunner.cs&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Raw results: &lt;a href="https://github.com/hellohanostudio/BuildAnalyzer/blob/main/Results/experiment-results-run1.csv" rel="noopener noreferrer"&gt;&lt;code&gt;experiment-results-run1.csv&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/hellohanostudio/BuildAnalyzer/blob/main/Results/experiment-results-run2.csv" rel="noopener noreferrer"&gt;&lt;code&gt;experiment-results-run2.csv&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Per-step BuildReport exports: &lt;a href="https://github.com/hellohanostudio/BuildAnalyzer/tree/main/Results/raw" rel="noopener noreferrer"&gt;&lt;code&gt;Results/raw&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unity API: &lt;a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Build.Reporting.BuildReport-packedAssets.html" rel="noopener noreferrer"&gt;&lt;code&gt;BuildReport.packedAssets&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Build.Reporting.PackedAssetInfo.html" rel="noopener noreferrer"&gt;&lt;code&gt;PackedAssetInfo&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Font: &lt;a href="https://github.com/google/fonts/tree/main/ofl/notosanskr" rel="noopener noreferrer"&gt;Noto Sans KR from Google Fonts&lt;/a&gt;, SIL Open Font License 1.1&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>android</category>
      <category>performance</category>
    </item>
    <item>
      <title>5 Reasons Your Unity Mobile UI Breaks Across Devices</title>
      <dc:creator>HanoStudio</dc:creator>
      <pubDate>Fri, 12 Jun 2026 15:14:26 +0000</pubDate>
      <link>https://dev.to/hano/5-reasons-your-unity-mobile-ui-breaks-across-devices-5a94</link>
      <guid>https://dev.to/hano/5-reasons-your-unity-mobile-ui-breaks-across-devices-5a94</guid>
      <description>&lt;p&gt;&lt;em&gt;And a Practical Pre-Release Checklist for Safe Areas, Aspect Ratios, and Localization&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mobile UI often looks fine in the Unity Editor.&lt;/p&gt;

&lt;p&gt;Then you test it on a real device.&lt;/p&gt;

&lt;p&gt;The top currency bar is too close to the notch. The bottom button overlaps with the home indicator. A popup looks fine on 16:9, but gets cut off on a smaller phone. German or French text overflows outside the button. The tablet layout feels stretched and empty.&lt;/p&gt;

&lt;p&gt;After 14 years of shipping Unity mobile games, these are the five UI issues I keep seeing across real projects.&lt;/p&gt;

&lt;p&gt;They are not always caused by bad UI design. Most of the time, they come from small assumptions that only break when the game is tested across different devices, safe areas, and languages.&lt;/p&gt;

&lt;p&gt;In this post, I want to share 5 common reasons Unity mobile UI breaks across devices, and a simple checklist you can use before release.&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%2Fh5sqfp6g71qfplo7wk4p.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%2Fh5sqfp6g71qfplo7wk4p.png" alt="The same Unity mobile menu before and after a UI QA scan"&gt;&lt;/a&gt;&lt;br&gt;
The same Unity mobile menu before and after a UI QA scan&lt;/p&gt;

&lt;p&gt;The same menu before and after a UI QA scan.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;1. Applying Safe Area to Everything&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of the most common mistakes is applying Safe Area to the entire Canvas.&lt;/p&gt;

&lt;p&gt;At first, this feels safe. If everything is inside the Safe Area, nothing should overlap with the notch or home indicator, right?&lt;/p&gt;

&lt;p&gt;But in practice, this often creates another problem.&lt;/p&gt;

&lt;p&gt;Backgrounds, dim overlays, screen effects, and full-screen visuals should usually fill the entire screen. If you place them inside the Safe Area, the screen can look visually cropped or incomplete.&lt;/p&gt;

&lt;p&gt;A better structure is to separate full-screen visuals from important interactive UI.&lt;/p&gt;

&lt;p&gt;Example structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Canvas
├── FullScreenLayer
│   ├── Background
│   ├── Dim
│   └── ScreenEffect
│
├── SafeAreaLayer
│   ├── TopHUD
│   ├── CurrencyArea
│   ├── NavigationButtons
│   └── BottomMenu
│
└── PopupLayer
    ├── DimBackground
    └── PopupRoot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backgrounds and full-screen effects can extend to the full screen.&lt;/li&gt;
&lt;li&gt;Buttons, HUD, currency, navigation, and readable UI should stay inside the Safe Area.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Safe Area is not for everything. It is mainly for important UI that the player needs to read or tap.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Testing Only One Aspect Ratio&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Many UI bugs are not visible when testing only 16:9.&lt;/p&gt;

&lt;p&gt;Mobile devices have many different screen shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 16:9
 18:9
 19.5:9
 20:9
 4:3
 Tablet ratios
 Foldable or unusual ratios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A layout that looks good on one phone can easily break on another.&lt;/p&gt;

&lt;p&gt;Common problems include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bottom buttons moving too close to the edge&lt;/li&gt;
&lt;li&gt;Top HUD stretching too much&lt;/li&gt;
&lt;li&gt;Popups becoming too tall&lt;/li&gt;
&lt;li&gt;Side margins looking too wide on tablets&lt;/li&gt;
&lt;li&gt;Important UI elements being pushed outside the screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When testing Unity mobile UI, it is better to check layout behavior across multiple aspect ratios before release.&lt;/p&gt;

&lt;p&gt;At minimum, I like to test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Small phone
 Tall phone
 iPhone-style notch device
 Android device with navigation bar
 Tablet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if you do not have all physical devices, using Unity Device Simulator or custom Game View resolutions can help catch many layout problems early.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Ignoring Localization Text Overflow&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Localization can break a layout that looked perfectly fine in English.&lt;/p&gt;

&lt;p&gt;A short English button label can become much longer in other languages.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Start
 Start Game
 Spiel starten
 Commencer le jeu
 게임 시작하기
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your UI was only tested with English text, you may miss issues like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Button text overflowing&lt;/li&gt;
&lt;li&gt;Popup titles becoming two lines&lt;/li&gt;
&lt;li&gt;Labels overlapping icons&lt;/li&gt;
&lt;li&gt;Text being clipped inside fixed-size containers&lt;/li&gt;
&lt;li&gt;Smaller devices not having enough horizontal space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially important for mobile UI because the available space is already limited.&lt;/p&gt;

&lt;p&gt;When testing localized UI, check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; [  ] Long button labels
 [  ] Popup titles
 [  ] Shop item names
 [  ] Mission descriptions
 [  ] Error messages
 [  ] Settings menu labels
 [  ] Small screen layouts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Localization QA should not happen only at the end. It should be part of layout testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. Making Popups Too Rigid&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Popups are another common source of mobile UI bugs.&lt;/p&gt;

&lt;p&gt;A popup may look fine on a large phone, but on a smaller device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The title gets too close to the top&lt;/li&gt;
&lt;li&gt;The content area becomes too tall&lt;/li&gt;
&lt;li&gt;The confirm button is pushed near the home indicator&lt;/li&gt;
&lt;li&gt;The popup is clipped vertically&lt;/li&gt;
&lt;li&gt;Text cannot fit inside the panel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A safer popup structure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; PopupRoot
 ├── Title
 ├── ScrollContent
 └── ButtonArea
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key idea is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the title visible.&lt;/li&gt;
&lt;li&gt;Keep the main buttons visible.&lt;/li&gt;
&lt;li&gt;Let the content area scroll if the screen is too small.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of making the entire popup fixed-height, consider giving it a maximum height and allowing the content area to shrink or scroll.&lt;/p&gt;

&lt;p&gt;This is especially useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terms popups&lt;/li&gt;
&lt;li&gt;Mission descriptions&lt;/li&gt;
&lt;li&gt;Shop item details&lt;/li&gt;
&lt;li&gt;Event notices&lt;/li&gt;
&lt;li&gt;Settings panels&lt;/li&gt;
&lt;li&gt;Localization-heavy UI&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Trusting the Editor Too Much&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Unity Editor is useful, but it does not always represent the real device experience.&lt;/p&gt;

&lt;p&gt;A UI can look fine in the Editor and still fail on device because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safe Area differences&lt;/li&gt;
&lt;li&gt;Device-specific navigation bars&lt;/li&gt;
&lt;li&gt;Notches and rounded corners&lt;/li&gt;
&lt;li&gt;DPI and scaling differences&lt;/li&gt;
&lt;li&gt;Touch comfort issues&lt;/li&gt;
&lt;li&gt;Real font rendering&lt;/li&gt;
&lt;li&gt;Platform-specific behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before release, it is important to test on actual devices when possible.&lt;/p&gt;

&lt;p&gt;But even before device testing, you can catch many problems with a structured checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A Simple Unity Mobile UI Pre-Release Checklist&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here is a practical checklist I use when reviewing mobile UI before release.&lt;/p&gt;

&lt;p&gt;Safe Area&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; [  ] Top HUD does not overlap with the notch
 [  ] Bottom buttons do not overlap with the home indicator
 [  ] Important buttons are inside the Safe Area
 [  ] Backgrounds still fill the entire screen
 [  ] Full-screen effects are not accidentally constrained by Safe Area
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aspect Ratio&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; [  ] UI works on 16:9
 [  ] UI works on tall phones
 [  ] UI works on small phones
 [  ] UI works on tablets
 [  ] UI does not stretch too much on wide layouts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Popup&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; [  ] Popup fits on small screens
 [  ] Main action buttons are always visible
 [  ] Long content can scroll
 [  ] Close button is easy to tap
 [  ] Popup title does not overlap with other elements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Localization&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; [  ] Long translated button text fits
 [  ] Popup titles support longer text
 [  ] Text does not overlap with icons
 [  ] Important labels are not clipped
 [  ] Small screens are tested with long strings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Touch Comfort&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; [  ] Buttons are not too close to screen edges
 [  ] Important buttons have enough padding
 [  ] Bottom UI is not too close to the home indicator
 [  ] Close buttons are easy to tap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Final Thoughts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Unity mobile UI does not usually break because of one big mistake.&lt;/p&gt;

&lt;p&gt;It breaks because of many small assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;assuming one aspect ratio is enough&lt;/li&gt;
&lt;li&gt;assuming English text is representative&lt;/li&gt;
&lt;li&gt;assuming Safe Area should apply to everything&lt;/li&gt;
&lt;li&gt;assuming popups will always fit&lt;/li&gt;
&lt;li&gt;assuming the Editor view is close enough to real devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best way to reduce these issues is to test UI with a clear checklist before release.&lt;/p&gt;

&lt;p&gt;Safe Area, aspect ratios, localization, popups, and touch comfort should be reviewed together.&lt;/p&gt;

&lt;p&gt;That is what helps catch layout problems before players do.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Resource&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Going through this checklist manually across every device ratio and language takes a long time — I know because I did it for years.&lt;/p&gt;

&lt;p&gt;So I built ScreenFit QA, a Unity editor tool that runs these checks automatically.&lt;/p&gt;

&lt;p&gt;It scans your UI across multiple device resolutions, safe areas, and long localized strings, then reports broken layouts before release.&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%2Fm4fx6lvp36npg12konb3.gif" 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%2Fm4fx6lvp36npg12konb3.gif" alt="ScreenFit QA finding safe-area, touch-target, text-overflow, and localization issues"&gt;&lt;/a&gt;&lt;br&gt;
ScreenFit QA finding safe-area, touch-target, text-overflow, and localization issues&lt;/p&gt;

&lt;p&gt;If this article's checklist felt useful, ScreenFit is that checklist, automated:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://hanostudio.gumroad.com/l/screenfit" rel="noopener noreferrer"&gt;See ScreenFit QA on Gumroad&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>mobile</category>
      <category>ui</category>
    </item>
  </channel>
</rss>
