<?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: Kylmora</title>
    <description>The latest articles on DEV Community by Kylmora (@kylmora).</description>
    <link>https://dev.to/kylmora</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%2F4122262%2F33bdfd10-22ac-437a-9571-0258ad25866b.png</url>
      <title>DEV Community: Kylmora</title>
      <link>https://dev.to/kylmora</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kylmora"/>
    <language>en</language>
    <item>
      <title>Six things about WKWebView the docs don't tell you</title>
      <dc:creator>Kylmora</dc:creator>
      <pubDate>Sat, 19 Sep 2026 03:45:55 +0000</pubDate>
      <link>https://dev.to/kylmora/six-things-about-wkwebview-the-docs-dont-tell-you-3c08</link>
      <guid>https://dev.to/kylmora/six-things-about-wkwebview-the-docs-dont-tell-you-3c08</guid>
      <description>&lt;p&gt;I have spent the last few months building a browser on WKWebView. Not a wrapper around a single web view — a real one, with tabs, separate cookie jars per workspace, downloads, extensions, the lot. WKWebView turns out to be a very good engine to build on and a slightly under-documented one. The class references are accurate about the API surface. What they do not tell you is what happens when you use several parts of it together.&lt;/p&gt;

&lt;p&gt;Here are the six things that cost me the most time. All macOS, though most of it applies to iOS too.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. WKProcessPool does nothing anymore
&lt;/h2&gt;

&lt;p&gt;Every tutorial written before about 2021 tells you to share a &lt;code&gt;WKProcessPool&lt;/code&gt; between web views so they share a process. Apple deprecated it in macOS 12 and it no longer influences process sharing at all. Creating one is dead weight.&lt;/p&gt;

&lt;p&gt;What actually decides process sharing now is the data store. Web views that share a &lt;code&gt;WKWebsiteDataStore&lt;/code&gt; share WebKit's network and content processes. Web views with different stores do not.&lt;/p&gt;

&lt;p&gt;That is a more useful lever than the process pool ever was, because it means isolation and cost are the same decision. If you want two groups of tabs genuinely separated — different cookies, different local storage — give them different stores and accept another set of processes. If you want them cheap, give them the same store.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;WKWebsiteDataStore(forIdentifier:)&lt;/code&gt; returns a new object every time
&lt;/h2&gt;

&lt;p&gt;macOS 14 added persistent, named data stores, which is the API you want if you are building anything with profiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;WKWebsiteDataStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it twice with the same UUID and you get two distinct objects pointing at the same data on disk. Nothing crashes. Nothing warns you. That is exactly the problem: you have silently given up the process sharing from point 1, because as far as WebKit is concerned those web views are in different stores.&lt;/p&gt;

&lt;p&gt;So cache them, and hand the same object out every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKWebsiteDataStore&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;dataStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;WKWebsiteDataStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;identity&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;existing&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKWebsiteDataStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isolated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;WKWebsiteDataStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;ephemeral&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nonPersistent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. You cannot delete a data store while anything still holds it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WKWebsiteDataStore.remove(forIdentifier:)&lt;/code&gt; throws &lt;code&gt;Data store is in use (by network process)&lt;/code&gt; if any reference to that store is still alive. That includes the one in the cache you just built in point 2, and it includes a web view you have already pulled out of the view hierarchy but are still holding somewhere.&lt;/p&gt;

&lt;p&gt;Retrying on its own does not fix it — I measured that, and the retries fail for as long as the reference exists. What works is dropping your last reference &lt;em&gt;first&lt;/em&gt;, then retrying a few times to cover the gap between the release and the deallocation actually landing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nf"&gt;releaseDataStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// drop the cached object&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;WKWebsiteDataStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;identifier&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;milliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;250&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 the asymmetry worth knowing: you cannot remove the default store at all. For that one you empty it with &lt;code&gt;removeData(ofTypes:modifiedSince:)&lt;/code&gt; instead. Two different APIs for the same user-facing "delete everything", and you need both if you support more than one kind of profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;window.open&lt;/code&gt; hands you a configuration, and you have to use it
&lt;/h2&gt;

&lt;p&gt;The delegate method looks harmless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;createWebViewWith&lt;/span&gt; &lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKWebViewConfiguration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;navigationAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKNavigationAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="nv"&gt;windowFeatures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKWindowFeatures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is tempting to ignore that &lt;code&gt;configuration&lt;/code&gt; and build the new web view with your own, because your own is the one with all your content scripts and settings attached. Do not. The new web view has to be created with the configuration WebKit handed you, or the opener relationship breaks: &lt;code&gt;window.opener&lt;/code&gt; comes back null, named targets stop finding the window, and OAuth popups that post back to their opener quietly fail.&lt;/p&gt;

&lt;p&gt;So the shape you want is a function that wraps &lt;em&gt;someone else's&lt;/em&gt; configuration rather than one that always makes its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;makeWebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSRect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKWebViewConfiguration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;webView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowsBackForwardNavigationGestures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowsMagnification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything you set on the &lt;code&gt;WKWebView&lt;/code&gt; itself is fine here. Anything you attach at configuration time — user scripts, message handlers, content rules — has to be attached in that delegate too, because this configuration never went through your own setup path.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The web inspector has two switches, for two different things
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;webView.isInspectable = true&lt;/code&gt; (macOS 13.3+) is what lets Safari's Develop menu attach to your web views. It is not what puts &lt;em&gt;Inspect Element&lt;/em&gt; in the page's own right-click menu. That is still the old preference, set by key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isInspectable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preferences&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"developerExtrasEnabled"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need both if you want the context menu item inside your own app. I spent an embarrassing afternoon on this one, convinced &lt;code&gt;isInspectable&lt;/code&gt; was broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Downloads live on the web view, not on &lt;code&gt;WKDownload&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WKDownload&lt;/code&gt; looks self-contained, and then you try to implement retry. The two methods that start a download — &lt;code&gt;resumeDownload(fromResumeData:)&lt;/code&gt; and &lt;code&gt;startDownload(using:)&lt;/code&gt; — are methods on &lt;code&gt;WKWebView&lt;/code&gt;, not on &lt;code&gt;WKDownload&lt;/code&gt;. So "retry this download" means keeping a live web view around for it, potentially long after the tab that started it has been closed.&lt;/p&gt;

&lt;p&gt;The second half of the same problem: &lt;code&gt;WKDownload.delegate&lt;/code&gt; is weak, so something has to own it. One delegate serving every transfer is far less bookkeeping than one per transfer — every callback hands you the &lt;code&gt;WKDownload&lt;/code&gt; back, so you can look up which row it belongs to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;download&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKDownload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;decideDestinationUsing&lt;/span&gt; &lt;span class="nv"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URLResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;suggestedFilename&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="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;download&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Five of these six are the same mistake in different clothes: treating a WebKit object as if it were a value you can make freely, when it is really a handle to a shared, process-backed resource with a lifetime. The data store, the configuration, the download — WebKit hands them to you, and the right move is almost always to hold on to exactly one of the thing and pass it around, rather than to construct a fresh one because constructing one is easy.&lt;/p&gt;

&lt;p&gt;If you have hit a seventh, I would genuinely like to hear it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All of the above came out of building &lt;a href="https://kylmora.com" rel="noopener noreferrer"&gt;Kylmora&lt;/a&gt;, a small open-source macOS browser — AppKit over the system WebKit, no bundled engine. The source is on &lt;a href="https://github.com/kylmora/kylmora" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you want to see any of these in context.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>macos</category>
      <category>webdev</category>
      <category>ios</category>
    </item>
    <item>
      <title>I built a native macOS browser with no dependencies - my first open source project</title>
      <dc:creator>Kylmora</dc:creator>
      <pubDate>Wed, 16 Sep 2026 13:39:16 +0000</pubDate>
      <link>https://dev.to/kylmora/i-built-a-native-macos-browser-with-no-dependencies-my-first-open-source-project-2eg3</link>
      <guid>https://dev.to/kylmora/i-built-a-native-macos-browser-with-no-dependencies-my-first-open-source-project-2eg3</guid>
      <description>&lt;p&gt;I've been using Arc, then Zen, then back to Safari, and I kept running into the same thing: either the browser was fast and rigid, or it was flexible and ate my RAM. So over the last few months I built the one I wanted, and last week I put it on GitHub.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;&lt;a href="https://kylmora.com" rel="noopener noreferrer"&gt;Kylmora&lt;/a&gt;&lt;/strong&gt;. It's an AppKit app over the system WebKit — the same engine Safari uses — with one window and Spaces inside it. No bundled Chromium, no Swift package dependencies, nothing but Apple's own frameworks.&lt;/p&gt;

&lt;p&gt;This is my first open source project, so please be gentle with it. If something is broken or missing, tell me and I'll fix it as fast as I can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no bundled engine
&lt;/h2&gt;

&lt;p&gt;Every Electron or Chromium-based browser ships a copy of the web. That's where the hundreds of megabytes come from, and it's why they're slow to launch.&lt;/p&gt;

&lt;p&gt;macOS already has WebKit in it. If you build on &lt;code&gt;WKWebView&lt;/code&gt;, you get the engine for free, you get Apple's security updates for free, and your app is just the shell around it. The whole of Kylmora is a few hundred Swift files.&lt;/p&gt;

&lt;p&gt;Here's what that costs, measured with 35 tabs restored and one page showing:&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;Release bundle&lt;/td&gt;
&lt;td&gt;5.2 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window on screen&lt;/td&gt;
&lt;td&gt;0.62 s warm, 0.97 s cold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser process at the window&lt;/td&gt;
&lt;td&gt;38 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebKit content process, one page&lt;/td&gt;
&lt;td&gt;~200 MB — that's the page's, not mine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trade is real and I want to be honest about it: you get Safari's engine, which means Safari's web compatibility and Safari's quirks. If a site only works in Chrome, it will only work in Chrome here too. I decided that was worth 5 MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spaces are identities, not just tab groups
&lt;/h2&gt;

&lt;p&gt;This is the part I actually built the browser for. Each Space has its own cookie jar, so the same site can be signed in as a different account in each one. Work Gmail in one, personal in another, a client's dashboard in a third — no profile switching, no separate windows.&lt;/p&gt;

&lt;p&gt;Each Space gets its own look (light/dark, colour or gradient, a rim around the window) so a glance at any corner of the screen tells you which identity is in front. And Space Routing rules send matching addresses to the right Space, so a link from Slack lands where it belongs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tabs that aren't there until you look at them
&lt;/h2&gt;

&lt;p&gt;A tab you haven't shown has no web view and no WebKit process behind it. Twenty-five restored tabs cost one content process, because twenty-four of them are still just a row in the sidebar with a title and a URL.&lt;/p&gt;

&lt;p&gt;Background tabs sleep after an idle period and under memory pressure, then come back where they were — scroll position, form contents, history.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few other things that are in there
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Boosts&lt;/strong&gt; — your own CSS and JS per site, plus a universal dark mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split view&lt;/strong&gt; — side by side, stacked, or 2×2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live folders&lt;/strong&gt; — a folder that fills itself from an RSS feed or a GitHub repo's issues and PRs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command palette&lt;/strong&gt; (⌘K) that ranks tabs, Spaces, history, bookmarks and commands together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation on Apple's on-device model&lt;/strong&gt; — the text never leaves the Mac&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensions&lt;/strong&gt; on WebKit's own engine, which reads the Chrome/Firefox manifest format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content blocking&lt;/strong&gt; on WebKit content rule lists, fed by EasyList and uBlock Origin's filters&lt;/li&gt;
&lt;li&gt;Passwords in the macOS login Keychain, the same store Safari uses, so iCloud Keychain just works&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's deliberately not in it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No AI.&lt;/strong&gt; No assistant, no summariser, no chat sidebar. Nothing about you or your Mac is sent anywhere. The update check asks for a JSON file once a day and that's the only request the app makes on its own.&lt;/p&gt;

&lt;p&gt;Also: nothing in the UI is a mock. If a control is there, it does something. Where a feature genuinely can't work on macOS, it says so instead of pretending.&lt;/p&gt;

&lt;h2&gt;
  
  
  Please break it
&lt;/h2&gt;

&lt;p&gt;It's Apache-2.0, macOS 14+, and there's a signed, notarized &lt;code&gt;.dmg&lt;/code&gt; on the releases page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/kylmora/kylmora" rel="noopener noreferrer"&gt;https://github.com/kylmora/kylmora&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://kylmora.com" rel="noopener noreferrer"&gt;https://kylmora.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issues: &lt;a href="https://github.com/kylmora/kylmora/issues" rel="noopener noreferrer"&gt;https://github.com/kylmora/kylmora/issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm one person and this is my first open source project, so I'm sure there's plenty I've got wrong. &lt;strong&gt;If you hit a bug, open an issue and I'll try to have it fixed within a day or two.&lt;/strong&gt; Same for features — if there's something your current browser does that you'd miss, tell me and I'll look at whether it fits. A lot of what's already in there came from people describing what they wanted somewhere in a thread.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Any feedback at all is genuinely welcome.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>macos</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
