Smaller HTML Payloads with Service Workers — Philip Walton

  • Home
  • Blog
  • Link
  • Smaller HTML Payloads with Service Workers — Philip Walton
nc efi placeholder

Many developers know that you can use service workers to cache web pages (and their sub-resources) in order to serve those pages to users when they’re offline.

And while this is true, it’s far from the only thing that service workers can do to improve the performance and reliability of a website. A lesser known capability of service workers is that you can programmatically generate your responses—you aren’t limited to just fetching from the network or reading from the cache.

In a traditional client-server setup, the server always needs to send a full HTML page to the client for every request (otherwise the response would be invalid). But when you think about it, that’s pretty wasteful. Most sites on the internet have a lot of repetition in their HTML payloads because their pages share a lot of common elements (e.g. the , navigation bars, banners, sidebars, footers etc.). But in an ideal world, you wouldn’t have to send so much of the same HTML, over and over again, with every single page request.

With service workers, there’s a solution to this problem. A service worker can request just the bare minimum of data it needs from the server (e.g. an HTML content partial, a Markdown file, JSON data, etc.), and then it can programmatically transform that data into a full HTML document.

On this site, after a user visits once and the service worker is installed, that user will never request a full HTML page again. Instead the service worker will intercept requests for pages and just request the contents of those pages—everything inside the

element—and then the service worker will combine that content with the rest of the HTML, which is already in the cache.

By only requesting the contents of a page, the networks payloads become substantially smaller, and the pages can load quite a bit faster. For example, on this site over the past 30 days, page loads from a service worker had a 47.6% smaller network payloads, and a median First Contentful Paint (FCP) that was 52.3% faster than page loads without a service worker (416ms vs. 851ms). In the graph below, you can clearly see the entire distribution shifted to the left:

First Contentful Paint (FCP) distribution by service worker status

How it works

Anyone who’s ever built a Single Page Application (SPA) is probably familiar with the basics of how this technique works. SPAs will typically only fetch the content portion of a new page and then swap that out with the content of the existing page—preventing the browser from having to make a full navigation.

Service workers can take this technique to the next level, though, since (once installed) they work for all page loads, not just in-page links. They can also leverage streaming APIs to deliver content even faster and let the browser start rendering even earlier—something SPAs can’t currently do (at least not without hacks).

When a user with a service worker installed visits any of my pages, the final HTML document the browser renders is actually a concatenation of three different page partials:

  • /shell-start.html
  • //index.content.html
  • /shell-end.html

And only one of those partials (the content) is sent over the network.

The following sections outline exactly how I’ve implemented this strategy on this site.

1) Create both a full and a content-only version of each page

In order to serve either a full HTML version of a page (for first-time visitors) or just a content partial (for repeat visitors with a service worker installed), you’ll need to either:

  • For dynamic sites: configure your server to conditionally render different templates based on the request.
  • For static sites: build two versions of each page.

Since this site is a static site, I do the latter. It might sound like a lot of extra work, but if you’re using a template system to build your pages, you’ve probably already extracted the common parts of your layout into partials. So the only thing left to do is create a content-only template and update your build process to render each page twice.

On this site I have a content partial template and then also a full page template that includes the content partial template in its

element.

Here’s an example of both rendered versions of my “About” page (note the view-source: URL prefix):

  • view-source:https://philipwalton.com/about/index.html
  • view-source:https://philipwalton.com/about/index.content.html

2) Create separate partials for the page shell

In order for the service worker to insert the page partial sent from your server into a full HTML page response that can be rendered in your browser window, it has to know what the surrounding HTML is for the full page.

The easiest way to make that work is to build and deploy this HTML as two separate files:

  • Everything that comes before the opening
    tag (including everything in the ).
  • Everything after the closing
tag.

On my site, I call these files shell-start.html and shell-end.html, and you can see their contents for yourself here:

  • view-source:https://philipwalton.com/shell-start.html
  • view-source:https://philipwalton.com/shell-end.html

I never request these files from the main page, but I do precache them in the service worker at install time, which I’ll explain next.

3) Store the shell partials in the cache

When a user first visits my site and the service worker installs, as part of the install event I fetch the contents of shell-start.html and shell-end.html, and put them in the cache storage.

I use Workbox (specifically the workbox-precaching package) to do this, which makes it easy to handle asset versioning and cache invalidation whenever I update either of these partials.

import {precache} from 'workbox-precaching';

precache([
  {url: '/shell-start.html', revision: SHELL_START_REV},
  {url: '/shell-end.html', revision: SHELL_END_REV},
  
]);

In the above code, the revision property of each precached URL is generated at build time using the rev-hash package and inserted in the service worker script via Rollup (rollup-plugin-replace).

Alternatively, if you don’t want to generate the revisions yourself, you can use the workbox-webpack-plugin, workbox-build, or workbox-cli packages to generate them for you. When doing that, your code would just look like this (and in your configuration you’d tell Workbox what files you want to revision, and it’ll generate the precache manifest for you, replacing the self.__WB_MANIFEST variable in your output file):

import {precache} from 'workbox-precaching';

precache(self.__WB_MANIFEST);

4) Configure your service worker to combine the content and shell partials

Once you’ve put the shell partials in the cache, the next step is to configure navigation requests to construct their responses by combining the shell partials from the cache with the content partial from the network.

A naive way to do this would be to get the text of each response and concatenate them together to form a new response:

import {getCacheKeyForURL} from 'workbox-precaching';

function getText(responsePromise) {
  return responsePromise.then((response) => response.text());
}

addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(async function() {
      const textPartials = await Promise.all([
        getText(caches.match(getCacheKeyForURL('/shell-start.html'))),
        getText(fetch(event.request.url + 'index.content.html')),
        getText(caches.match(getCacheKeyForURL('/shell-end.html'))),
      ]);

      return new Response(textPartials.join(''), {
        headers: {'content-type': 'text/html'},
      });
    }());
  }
});

I said above that this is the naive way to do it, not because it won’t work, but because it requires you to wait for all three responses to fully complete before you can even begin to deliver any of the response to the page.

All modern browsers and servers support sending and receiving HTML as a stream of content, and service workers are no different. So instead of waiting until you have the full text of each response and then creating a new response from that full string, you can create a ReadableStream and start responding as soon as you have the very first bit of content. And since the shell-start.html file will be coming from the cache, you can generally start responding right away—you don’t need to wait for the network request to finish!

If you’ve never heard of Readable Streams before, don’t worry. When using Workbox (which I recommend) you don’t have to deal with them directly. The workbox-streams package has a utility method for creating a streaming response by combining other runtime caching strategies.

import {cacheNames} from 'workbox-core';
import {getCacheKeyForURL} from 'workbox-precaching';
import {registerRoute} from 'workbox-routing';
import {CacheFirst, StaleWhileRevalidate} from 'workbox-strategies';
import {strategy as composeStrategies} from 'workbox-streams';

const shellStrategy = new CacheFirst({cacheName: cacheNames.precache});
const contentStrategy = new StaleWhileRevalidate({cacheName: 'content'});

const navigationHandler = composeStrategies([
  () => shellStrategy.handle({
    request: new Request(getCacheKeyForURL('/shell-start.html')),
  }),
  ({url}) => contentStrategy.handle({
    request: new Request(url.pathname + 'index.content.html'),
  }),
  () => shellStrategy.handle({
    request: new Request(getCacheKeyForURL('/shell-end.html')),
  }),
]);

registerRoute(({request}) => request.mode === 'navigate', navigationHandler);

In the above code I’m using a cache-first strategy for the shell partials, and then a stale-while-revalidate strategy for the content partials. This means users who revisit a page they already have cached might see stale content, but it also means that content will load instantly.

If you prefer to always get fresh content from the network, you can use a network-first strategy instead.

Note: A nice benefit of using workbox-streams is that it’ll automatically fallback to a full text response in browsers that don’t support Readable Streams (though at this point all modern browsers support streams).

5) Set the correct title

Observant readers might have noticed that, if you serve the same cached shell content for all pages, you’ll end up having the same </code> tag for every page, as well as any <code><link></code> or <code><meta></code> tags that had previously been page-specific.</p> <p>The best way to deal with this is for your page partials to include a script tag at the end that sets the title (and any other page-specific data) at runtime. For example, my page partial template uses something like this:</p> <pre><code class="language-html"><span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript"><span class="hljs-built_in">document</span>.title = <span class="hljs-string">'{{ page.title }}'</span></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span> </code></pre> <p>Note that this is not a problem for search crawlers or other services that render page preview cards. These tools do not run your service worker, which means they’ll always get the full HTML page when making a request.</p> <p>It’s also not a problem for users who have JavaScript disabled because, again, those users would not be running your service worker either.</p> <h2 id="performance-gains-(in-detail)">Performance gains (in detail)</h2> <p>The histogram I showed at the beginning of the article should give you a sense for how using this technique vastly improves FCP for all users. Here’s a closer look at the specific FCP values at some key percentiles:</p> <table> <tr> <th colspan="3">First Contentful Paint (in milliseconds)</th> </tr> <tr> <td><strong>Percentile</strong></td> <td><strong>Service Worker</strong></td> <td><strong>No Service Worker</strong></td> </tr> <tr> <td>50th</td> <td>416</td> <td>851</td> </tr> <tr> <td>75th</td> <td>701</td> <td>1264</td> </tr> <tr> <td>90th</td> <td>1181</td> <td>1965</td> </tr> <tr> <td>95th</td> <td>1797</td> <td>2632</td> </tr> </table> <p>As you can see, FCP is faster when using a service worker across all key percentiles.</p> <p>However, since visitors with a service worker installed are <em>always</em> returning visitors, and visitors without service worker installed are likely first time visitors, you might be skeptical as to whether the performance improvements I’m seeing are actually from this technique, or whether they’re from things like resource caching in general.</p> <p>While resource caching may improve FCP for some sites, it actually doesn’t for mine. I inline both my CSS and SVG content in the <code><head></code> of my pages, which means FCP is never blocked on anything other than the page response, and that means the FCP gains seen here are entirely due to how I’m generating the response in the service worker.</p> <p>The primary reason service worker loads are faster on this site is because users with a service worker installed already have the <code>shell-start.html</code> partial in their cache. And since the service worker is responding with a stream, <strong>the browser can start rendering the shell almost immediately—and it can fetch the page’s content from the server in parallel.</strong></p> <p>But that brings up another interesting question: <em>does this technique improve the speed of the entire response, or just the first part of it?</em></p> <p>Again, to answer that question let me show you some timing data for the entire response.</p> <p>And note that since I use a stale-while-revalidate caching strategy for my content partials, sometimes a user will already have a page’s content partial in the cache (e.g. if they’re returning to an article they’ve already read) and sometimes they won’t (e.g. they previously visited my site, and now they’ve come back to read a new article).</p> <p>Here’s the response timing data segmented by both service worker status as well as content partial cache status:</p> <table> <tr> <th colspan="4">Response Complete Time (in milliseconds)</th> </tr> <tr> <td><strong>Percentile</strong></td> <td><strong>Service Worker</strong><br /><em>(content cached)</em></td> <td><strong>Service Worker</strong><br /><em>(content not cached)</em></td> <td><strong>No Service Worker</strong></td> </tr> <tr> <td>50th</td> <td>92</td> <td>365</td> <td>480</td> </tr> <tr> <td>75th</td> <td>218</td> <td>634</td> <td>866</td> </tr> <tr> <td>90th</td> <td>520</td> <td>1017</td> <td>1497</td> </tr> <tr> <td>95th</td> <td>887</td> <td>1284</td> <td>2213</td> </tr> </table> <p>Comparing the performance results between the “No Service Worker” case and the “Service Worker (content not cached)” case is particularly interesting because in both cases the browser has to fetch something from the server over the network. But as you can see from these results, fetching just the content part of the HTML (rather than the entire page in the “no service worker” case) is around 20%-30% faster for most users—and that even includes the overhead of starting up the service worker thread if it’s not running!</p> <p>And if you look at the performance results for visitors who already had the content partial in their cache, you can see the responses are near instant for the majority of users!</p> <p><strong>Note:</strong> if you’re not familiar with how service worker thread startup time can affect performance, watch my talk on <a href="https://youtu.be/25aCD5XL1Jk" target="_blank" rel="noopener">Building Faster, More Resilient Apps with Service Worker from Chrome Dev Summit in 2018</p> </aside> <h2 id="key-takeaways">Key takeaways</h2> <p>This article has shown how you can use service workers to significantly reduce the amount of data your users need to request from your server, and as a result you can dramatically improve both the render and load times for your pages.</p> <p>To end, I want to emphasize a couple of key pieces of performance advice from this article that I hope will stick with you:</p> <ul> <li>When using a service worker, you have a lot more flexibility on <em>how</em> you can get data from your server. Use this flexibility to reduce data usage and improve performance!</li> <li>Never cache full HTML pages. Break up your pages into common chunks that can be cached separately. Caching granular chunks means things are less likely to get invalidated when you make changes.</li> <li>Avoid blocking first paint behind any resource requests (e.g. a stylesheet). For the initial visit you can inline your stylesheet in the <code><head></code>, and for returning visits (once the service worker has installed) all resources required for first paint should be in the cache.</li> </ul> <h2 id="additional-resources">Additional resources</h2> </div> <p><a href="https://philipwalton.com/articles/smaller-html-payloads-with-service-workers/" target="_blank" rel="noopener">Source link </a></p> </div> </div> </div> <div class="entry-footer"> <div class="entry-content-bottom"> <div class="entry-social"> <a class="fb-social hover-effect" title="Facebook" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=https://tech-chat.co.za/2020/02/01/smaller-html-payloads-with-service-workers-philip-walton/"><i class="zmdi zmdi-facebook"></i></a> <a class="tw-social hover-effect" title="Twitter" target="_blank" href="http://twitter.com/share?url=https://tech-chat.co.za/2020/02/01/smaller-html-payloads-with-service-workers-philip-walton/"><i class="zmdi zmdi-twitter"></i></a> <a class="g-social hover-effect" title="Google Plus" target="_blank" href="https://plus.google.com/share?url=https://tech-chat.co.za/2020/02/01/smaller-html-payloads-with-service-workers-philip-walton/"><i class="zmdi zmdi-google-plus"></i></a> <a class="pin-social hover-effect" title="Pinterest" target="_blank" href="https://pinterest.com/pin/create/button/?url=http://philipwalton.com/static/fcp-histogram-by-sw-status-4594d7c3fd.png&media=&description=Smaller HTML Payloads with Service Workers — Philip Walton"><i class="zmdi zmdi-pinterest"></i></a> </div> </div> <div class="post-previous-next"> <div class="post-previous"> <a href="https://tech-chat.co.za/2020/02/01/how-to-improve-outdoor-environmental-quality-of-residential-areas/"><i class="fa fa-angle-double-left"></i> Previous Post</a> </div> <div class="post-next"> <a href="https://tech-chat.co.za/2020/02/01/microsofts-new-office-build-12513-20010-adds-an-important-new-feature-to-excel/">Newer Post <i class="fa fa-angle-double-right"></i></a> </div> </div> </div> </article><!-- #post --> <div id="comments" class="comments-area"> <div class="comment-list-wrap"> <h2 class="comments-title"> 2 Comments </h2><!-- .comments-title --> <ul class="comment-list"> <li class="comment even thread-even depth-1" id="comment-766"> <div id="div-comment-766" class="comment-body"> <div class="comment-inner clearfix"> <img alt='' src='https://secure.gravatar.com/avatar/e39c2d382b5431aa16466adfaddaf411?s=32&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/e39c2d382b5431aa16466adfaddaf411?s=64&d=mm&r=g 2x' class='avatar avatar-32 photo' height='32' width='32' loading='lazy'/> <div class="comment-content"> <h4 class="comment-title">Tereasa Monn</h4> <span class="comment-date"> April 23, 2020 - 9:29 am </span> <div class="comment-text"><p>Bookmarked!, I like your blog!</p> </div> <div class="comment-reply"> <a rel='nofollow' class='comment-reply-link' href='#comment-766' data-commentid="766" data-postid="9132" data-belowelement="div-comment-766" data-respondelement="respond" data-replyto="Reply to Tereasa Monn" aria-label='Reply to Tereasa Monn'>Reply</a> </div> <div class="line-comment"></div> </div> </div> </div> </li><!-- #comment-## --> <li class="comment odd alt thread-odd thread-alt depth-1" id="comment-908"> <div id="div-comment-908" class="comment-body"> <div class="comment-inner clearfix"> <img alt='' src='https://secure.gravatar.com/avatar/6809473fd5cb5337f23ba0d60dea7192?s=32&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/6809473fd5cb5337f23ba0d60dea7192?s=64&d=mm&r=g 2x' class='avatar avatar-32 photo' height='32' width='32' loading='lazy'/> <div class="comment-content"> <h4 class="comment-title"><a href="http://msc.com.ru" rel="external nofollow ugc" class="url">MSCkerne</a></h4> <span class="comment-date"> April 1, 2021 - 11:29 am </span> <div class="comment-text"><p>Таможенная компания “Азия-Трейдинг”</p> </div> <div class="comment-reply"> <a rel='nofollow' class='comment-reply-link' href='#comment-908' data-commentid="908" data-postid="9132" data-belowelement="div-comment-908" data-respondelement="respond" data-replyto="Reply to MSCkerne" aria-label='Reply to MSCkerne'>Reply</a> </div> <div class="line-comment"></div> </div> </div> </div> </li><!-- #comment-## --> </ul><!-- .comment-list --> </div> <div id="respond" class="comment-respond"> <h3 id="reply-title" class="comment-reply-title">Leave A Reply <small><a rel="nofollow" id="cancel-comment-reply-link" href="/2020/02/01/smaller-html-payloads-with-service-workers-philip-walton/#respond" style="display:none;">Cancel Reply</a></small></h3><form action="https://tech-chat.co.za/wp-comments-post.php" method="post" id="commentform" class="comment-form" novalidate><div class="row"><div class="comment-form-author col-lg-4 col-md-4 col-sm-12"><input id="author" name="author" type="text" value="" size="30" placeholder="Name"/></div> <div class="comment-form-email col-lg-4 col-md-4 col-sm-12"><input id="email" name="email" type="text" value="" size="30" placeholder="Email"/></div> <div class="comment-form-website col-lg-4 col-md-4 col-sm-12"><input id="website" name="url" type="text" value="" size="30" placeholder="Website"/></div></div> <p class="comment-form-cookies-consent"><input id="wp-comment-cookies-consent" name="wp-comment-cookies-consent" type="checkbox" value="yes" /> <label for="wp-comment-cookies-consent">Save my name, email, and website in this browser for the next time I comment.</label></p> <div class="comment-form-comment"><textarea id="comment" name="comment" cols="45" rows="8" placeholder="Comment" aria-required="true"></textarea></div><p class="form-submit"><input name="submit" type="submit" id="submit" class="submit" value="Post Comment" /> <input type='hidden' name='comment_post_ID' value='9132' id='comment_post_ID' /> <input type='hidden' name='comment_parent' id='comment_parent' value='0' /> </p></form> </div><!-- #respond --> </div><!-- #comments --> </main><!-- #main --> </div><!-- #primary --> <aside id="secondary" class="widget-area widget-has-sidebar sidebar-fixed col-xl-3 col-lg-4 col-md-12"> <div class="sidebar-fixed-inner"> <section id="search-2" class="widget widget_search"><div class="widget-content"> <form role="search" method="get" class="search-form" action="https://tech-chat.co.za/"> <div class="searchform-wrap"> <input type="text" placeholder="Search..." name="s" class="search-field" /> <button type="submit" class="search-submit"><i class="fa fa-search"></i></button> </div> </form></div></section><section id="media_image-3" class="widget widget_media_image"><div class="widget-content"><img width="350" height="250" src="https://tech-chat.co.za/wp-content/uploads/2018/10/side_img1.jpg" class="image wp-image-2652 attachment-full size-full" alt="" loading="lazy" style="max-width: 100%; height: auto;" srcset="https://tech-chat.co.za/wp-content/uploads/2018/10/side_img1.jpg 350w, https://tech-chat.co.za/wp-content/uploads/2018/10/side_img1-300x214.jpg 300w" sizes="(max-width: 350px) 100vw, 350px" /></div></section><section id="tag_cloud-3" class="widget widget_tag_cloud"><div class="widget-content"><h2 class="widget-title">Tags</h2><div class="tagcloud"><a href="https://tech-chat.co.za/tag/5g/" class="tag-cloud-link tag-link-2799 tag-link-position-1" style="font-size: 8pt;" aria-label="5g (1 item)">5g</a> <a href="https://tech-chat.co.za/tag/agency/" class="tag-cloud-link tag-link-2099 tag-link-position-2" style="font-size: 8pt;" aria-label="Agency (1 item)">Agency</a> <a href="https://tech-chat.co.za/tag/armorlock/" class="tag-cloud-link tag-link-3098 tag-link-position-3" style="font-size: 8pt;" aria-label="ArmorLock (1 item)">ArmorLock</a> <a href="https://tech-chat.co.za/tag/gaming/" class="tag-cloud-link tag-link-2718 tag-link-position-4" style="font-size: 22pt;" aria-label="gaming (2 items)">gaming</a> <a href="https://tech-chat.co.za/tag/interserver/" class="tag-cloud-link tag-link-2100 tag-link-position-5" style="font-size: 8pt;" aria-label="interserver (1 item)">interserver</a> <a href="https://tech-chat.co.za/tag/mobilegaming/" class="tag-cloud-link tag-link-2801 tag-link-position-6" style="font-size: 8pt;" aria-label="mobilegaming (1 item)">mobilegaming</a> <a href="https://tech-chat.co.za/tag/ssd/" class="tag-cloud-link tag-link-2717 tag-link-position-7" style="font-size: 22pt;" aria-label="ssd (2 items)">ssd</a> <a href="https://tech-chat.co.za/tag/wd_black/" class="tag-cloud-link tag-link-2720 tag-link-position-8" style="font-size: 8pt;" aria-label="WD_Black (1 item)">WD_Black</a> <a href="https://tech-chat.co.za/tag/webhosting/" class="tag-cloud-link tag-link-2098 tag-link-position-9" style="font-size: 8pt;" aria-label="webhosting (1 item)">webhosting</a> <a href="https://tech-chat.co.za/tag/westerndigital/" class="tag-cloud-link tag-link-2719 tag-link-position-10" style="font-size: 22pt;" aria-label="WesternDigital: (2 items)">WesternDigital:</a></div> </div></section><section id="archives-1" class="widget widget_archive"><div class="widget-content"><h2 class="widget-title">Archives</h2> <label class="screen-reader-text" for="archives-dropdown-1">Archives</label> <select id="archives-dropdown-1" name="archive-dropdown"> <option value="">Select Month</option> <option value='https://tech-chat.co.za/2021/04/'> April 2021 </option> <option value='https://tech-chat.co.za/2021/03/'> March 2021 </option> <option value='https://tech-chat.co.za/2021/02/'> February 2021 </option> <option value='https://tech-chat.co.za/2021/01/'> January 2021 </option> <option value='https://tech-chat.co.za/2020/12/'> December 2020 </option> <option value='https://tech-chat.co.za/2020/11/'> November 2020 </option> <option value='https://tech-chat.co.za/2020/10/'> October 2020 </option> <option value='https://tech-chat.co.za/2020/09/'> September 2020 </option> <option value='https://tech-chat.co.za/2020/08/'> August 2020 </option> <option value='https://tech-chat.co.za/2020/07/'> July 2020 </option> <option value='https://tech-chat.co.za/2020/06/'> June 2020 </option> <option value='https://tech-chat.co.za/2020/05/'> May 2020 </option> <option value='https://tech-chat.co.za/2020/04/'> April 2020 </option> <option value='https://tech-chat.co.za/2020/03/'> March 2020 </option> <option value='https://tech-chat.co.za/2020/02/'> February 2020 </option> <option value='https://tech-chat.co.za/2020/01/'> January 2020 </option> <option value='https://tech-chat.co.za/2019/12/'> December 2019 </option> <option value='https://tech-chat.co.za/2019/11/'> November 2019 </option> </select> <script type="text/javascript"> /* <![CDATA[ */ (function() { var dropdown = document.getElementById( "archives-dropdown-1" ); function onSelectChange() { if ( dropdown.options[ dropdown.selectedIndex ].value !== '' ) { document.location.href = this.options[ this.selectedIndex ].value; } } dropdown.onchange = onSelectChange; })(); /* ]]> */ </script> </div></section><section id="ct_recent_posts-2" class="widget widget_ct_recent_posts"><div class="widget-content"><h2 class="widget-title">Recent Posts</h2><div class="posts-list"><div class="entry-brief"> <div class="entry-content"> <h4 class="entry-title"><a href="https://tech-chat.co.za/2021/04/10/magisk-what-is-it-and-how-to-install-android-apps/" title="Magisk: What Is It and How to Install Android Apps">Magisk: What Is It and How to Install Android Apps</a></h4> <ul class="entry-meta"> <li class="item-date"> <i class="fa fa-calendar"></i>April 10, 2021 </li> </ul> </div> </div> <div class="entry-brief"> <div class="entry-content"> <h4 class="entry-title"><a href="https://tech-chat.co.za/2021/04/09/is-it-a-lawnmower-is-it-an-rpi-irc-server-its-both/" title="Is It A Lawnmower? Is It an RPi IRC Server? It’s Both!">Is It A Lawnmower? Is It an RPi IRC Server? It’s Both!</a></h4> <ul class="entry-meta"> <li class="item-date"> <i class="fa fa-calendar"></i>April 9, 2021 </li> </ul> </div> </div> <div class="entry-brief"> <div class="entry-content"> <h4 class="entry-title"><a href="https://tech-chat.co.za/2021/04/09/report-that-google-pixel-5a-cancelled-false/" title="Report that Google Pixel 5a cancelled false">Report that Google Pixel 5a cancelled false</a></h4> <ul class="entry-meta"> <li class="item-date"> <i class="fa fa-calendar"></i>April 9, 2021 </li> </ul> </div> </div> </div></div></section><section id="text-3" class="widget widget_text"><div class="widget-content"><h2 class="widget-title">Gallery</h2> <div class="textwidget"><div class="wpb_gallery wpb_content_element vc_clearfix" ><div class="wpb_wrapper"><div class="wpb_gallery_slides wpb_image_grid" data-interval="3"><ul class="wpb_image_grid_ul"><li class="isotope-item"><a class="prettyphoto" href="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio3.jpg" data-rel="prettyPhoto[rel-9132-1013248471]"><img width="600" height="600" src="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio3.jpg" class="attachment-full" alt="" loading="lazy" srcset="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio3.jpg 600w, https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio3-300x300.jpg 300w" sizes="(max-width: 600px) 100vw, 600px" /></a></li><li class="isotope-item"><a class="prettyphoto" href="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio2.jpg" data-rel="prettyPhoto[rel-9132-1013248471]"><img width="800" height="800" src="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio2.jpg" class="attachment-full" alt="" loading="lazy" srcset="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio2.jpg 800w, https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio2-300x300.jpg 300w, https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio2-600x600.jpg 600w" sizes="(max-width: 800px) 100vw, 800px" /></a></li><li class="isotope-item"><a class="prettyphoto" href="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio4.jpg" data-rel="prettyPhoto[rel-9132-1013248471]"><img width="800" height="800" src="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio4.jpg" class="attachment-full" alt="" loading="lazy" srcset="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio4.jpg 800w, https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio4-300x300.jpg 300w, https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio4-600x600.jpg 600w" sizes="(max-width: 800px) 100vw, 800px" /></a></li><li class="isotope-item"><a class="prettyphoto" href="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio5.jpg" data-rel="prettyPhoto[rel-9132-1013248471]"><img width="800" height="800" src="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio5.jpg" class="attachment-full" alt="" loading="lazy" srcset="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio5.jpg 800w, https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio5-300x300.jpg 300w, https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio5-600x600.jpg 600w" sizes="(max-width: 800px) 100vw, 800px" /></a></li><li class="isotope-item"><a class="prettyphoto" href="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio1.jpg" data-rel="prettyPhoto[rel-9132-1013248471]"><img width="800" height="800" src="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio1.jpg" class="attachment-full" alt="" loading="lazy" srcset="https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio1.jpg 800w, https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio1-300x300.jpg 300w, https://tech-chat.co.za/wp-content/uploads/2018/10/portfolio1-600x600.jpg 600w" sizes="(max-width: 800px) 100vw, 800px" /></a></li><li class="isotope-item"><a class="prettyphoto" href="https://tech-chat.co.za/wp-content/uploads/2018/10/gallery-update1.jpg" data-rel="prettyPhoto[rel-9132-1013248471]"><img width="600" height="600" src="https://tech-chat.co.za/wp-content/uploads/2018/10/gallery-update1.jpg" class="attachment-full" alt="" loading="lazy" srcset="https://tech-chat.co.za/wp-content/uploads/2018/10/gallery-update1.jpg 600w, https://tech-chat.co.za/wp-content/uploads/2018/10/gallery-update1-300x300.jpg 300w" sizes="(max-width: 600px) 100vw, 600px" /></a></li></ul></div></div></div> </div> </div></section> </div> </aside> </div> </div> </div><!-- #content inner --> </div><!-- #content --> <footer id="colophon" class="site-footer footer-layout1"> <div class="top-footer top-width-custom"> <div class="container"> <div class="row"> <div class="ct-footer-item col-xl-4 col-lg-4 col-md-4 col-sm-12"> <section id="text-4" class="widget widget_text"><h2 class="footer-widget-title">About Us</h2> <div class="textwidget"><p>We recognize that the needs of one client differ from the next and so needs assessment and Roi are our primary priority</p> </div> </section> <div class="contact-info widget"> <ul class="ct-contact-info-inner"> <li> <i class="fa fa-phone"></i> <span>0878217888</span> </li> <li> <i class="fa fa-envelope"></i> <span>info@tech-chat.co.za</span> </li> <li> <i class="fa fa-home"></i> <span>440 Felstead Rd, North Riding AH, Roodepoort, 2169</span> </li> </ul> </div> </div><div class="ct-footer-item col-xl-4 col-lg-4 col-md-4 col-sm-12"> <section id="nav_menu-8" class="widget widget_nav_menu"><h2 class="footer-widget-title">Links</h2><div class="menu-links-container"><ul id="menu-links" class="menu"><li id="menu-item-13891" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-13891"><a href="#" class="item-one-page">Home</a></li> <li id="menu-item-13892" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-13892"><a href="#" class="item-one-page">Services</a></li> <li id="menu-item-13893" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-13893"><a href="#" class="item-one-page">About Us</a></li> <li id="menu-item-13894" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-13894"><a href="#" class="item-one-page">Testimonials</a></li> <li id="menu-item-13895" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-13895"><a href="#" class="item-one-page">News</a></li> <li id="menu-item-13896" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-13896"><a href="#" class="item-one-page">Contact</a></li> </ul></div></section></div><div class="ct-footer-item col-xl-4 col-lg-4 col-md-4 col-sm-12"> <section id="nav_menu-9" class="widget widget_nav_menu"><h2 class="footer-widget-title">Services</h2><div class="menu-services-container"><ul id="menu-services" class="menu"><li id="menu-item-3464" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-home menu-item-3464"><a href="https://tech-chat.co.za" class="item-one-page">Outsourced Support</a></li> <li id="menu-item-3465" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-home menu-item-3465"><a href="https://tech-chat.co.za" class="item-one-page">Digital Marketing</a></li> <li id="menu-item-3799" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-home menu-item-3799"><a href="https://tech-chat.co.za/" class="item-one-page">Custom Services</a></li> <li id="menu-item-3463" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-home menu-item-3463"><a href="https://tech-chat.co.za" class="item-one-page">Graphics Design</a></li> <li id="menu-item-3461" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-home menu-item-3461"><a href="https://tech-chat.co.za" class="item-one-page">Hosting</a></li> <li id="menu-item-3462" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-home menu-item-3462"><a href="https://tech-chat.co.za/" class="item-one-page">SEO</a></li> </ul></div></section></div> </div> </div> </div> <div class="bottom-footer"> <div class="container"> <div class="row"> <div class="bottom-copyright"> <p>2020 © All rights reserved by Tech-Chat Solutions Pty Ltd</p> </div> <div class="bottom-social"> <label>Follow us:</label> <a href="#" target="_blank"><i class="fa fa-facebook-square"></i></a><a href="#" target="_blank"><i class="fa fa-twitter-square"></i></a><a href="#" target="_blank"><i class="fa fa-linkedin-square"></i></a><a href="#" target="_blank"><i class="fa fa-instagram"></i></a> </div> </div> </div> </div> </footer> <a href="#" class="ct-scroll-top"> <i class="ti-angle-up"></i> </a> </div><!-- #page --> <script type="text/javascript"> jQuery( function($) { if ( typeof wc_add_to_cart_params === 'undefined' ) return false; $(document.body).on( 'added_to_cart', function( event, fragments, cart_hash, $button ) { var $pid = $button.data('product_id'); $.ajax({ type: 'POST', url: wc_add_to_cart_params.ajax_url, data: { 'action': 'item_added', 'id' : $pid }, success: function (response) { $('.ct-widget-cart-wrap').addClass('open'); } }); }); }); </script> <link rel='stylesheet' id='js_composer_front-css' href='https://tech-chat.co.za/wp-content/plugins/js_composer/assets/css/js_composer.min.css?ver=6.2.0' type='text/css' media='all' /> <link rel='stylesheet' id='isotope-css-css' href='https://tech-chat.co.za/wp-content/plugins/js_composer/assets/css/lib/isotope.min.css?ver=6.2.0' type='text/css' media='all' /> <link rel='stylesheet' id='prettyphoto-css' href='https://tech-chat.co.za/wp-content/plugins/js_composer/assets/lib/prettyphoto/css/prettyPhoto.min.css?ver=6.2.0' type='text/css' media='all' /> <script type='text/javascript' src='https://tech-chat.co.za/wp-includes/js/dist/vendor/wp-polyfill.min.js?ver=7.4.4' id='wp-polyfill-js'></script> <script type='text/javascript' id='wp-polyfill-js-after'> ( 'fetch' in window ) || document.write( '<script src="https://tech-chat.co.za/wp-includes/js/dist/vendor/wp-polyfill-fetch.min.js?ver=3.0.0"></scr' + 'ipt>' );( document.contains ) || document.write( '<script src="https://tech-chat.co.za/wp-includes/js/dist/vendor/wp-polyfill-node-contains.min.js?ver=3.42.0"></scr' + 'ipt>' );( window.DOMRect ) || document.write( '<script src="https://tech-chat.co.za/wp-includes/js/dist/vendor/wp-polyfill-dom-rect.min.js?ver=3.42.0"></scr' + 'ipt>' );( window.URL && window.URL.prototype && window.URLSearchParams ) || document.write( '<script src="https://tech-chat.co.za/wp-includes/js/dist/vendor/wp-polyfill-url.min.js?ver=3.6.4"></scr' + 'ipt>' );( window.FormData && window.FormData.prototype.keys ) || document.write( '<script src="https://tech-chat.co.za/wp-includes/js/dist/vendor/wp-polyfill-formdata.min.js?ver=3.0.12"></scr' + 'ipt>' );( Element.prototype.matches && Element.prototype.closest ) || document.write( '<script src="https://tech-chat.co.za/wp-includes/js/dist/vendor/wp-polyfill-element-closest.min.js?ver=2.0.2"></scr' + 'ipt>' );( 'objectFit' in document.documentElement.style ) || document.write( '<script src="https://tech-chat.co.za/wp-includes/js/dist/vendor/wp-polyfill-object-fit.min.js?ver=2.3.4"></scr' + 'ipt>' ); </script> <script type='text/javascript' src='https://tech-chat.co.za/wp-includes/js/dist/hooks.min.js?ver=50e23bed88bcb9e6e14023e9961698c1' id='wp-hooks-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-includes/js/dist/i18n.min.js?ver=db9a9a37da262883343e941c3731bc67' id='wp-i18n-js'></script> <script type='text/javascript' id='wp-i18n-js-after'> wp.i18n.setLocaleData( { 'text direction\u0004ltr': [ 'ltr' ] } ); </script> <script type='text/javascript' src='https://tech-chat.co.za/wp-includes/js/dist/vendor/lodash.min.js?ver=4.17.19' id='lodash-js'></script> <script type='text/javascript' id='lodash-js-after'> window.lodash = _.noConflict(); </script> <script type='text/javascript' src='https://tech-chat.co.za/wp-includes/js/dist/url.min.js?ver=0ac7e0472c46121366e7ce07244be1ac' id='wp-url-js'></script> <script type='text/javascript' id='wp-api-fetch-js-translations'> ( function( domain, translations ) { var localeData = translations.locale_data[ domain ] || translations.locale_data.messages; localeData[""].domain = domain; wp.i18n.setLocaleData( localeData, domain ); } )( "default", { "locale_data": { "messages": { "": {} } } } ); </script> <script type='text/javascript' src='https://tech-chat.co.za/wp-includes/js/dist/api-fetch.min.js?ver=a783d1f442d2abefc7d6dbd156a44561' id='wp-api-fetch-js'></script> <script type='text/javascript' id='wp-api-fetch-js-after'> wp.apiFetch.use( wp.apiFetch.createRootURLMiddleware( "https://tech-chat.co.za/wp-json/" ) ); wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware( "95d0d3c94e" ); wp.apiFetch.use( wp.apiFetch.nonceMiddleware ); wp.apiFetch.use( wp.apiFetch.mediaUploadMiddleware ); wp.apiFetch.nonceEndpoint = "https://tech-chat.co.za/wp-admin/admin-ajax.php?action=rest-nonce"; </script> <script type='text/javascript' id='contact-form-7-js-extra'> /* <![CDATA[ */ var wpcf7 = {"cached":"1"}; /* ]]> */ </script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/plugins/contact-form-7/includes/js/index.js?ver=5.4' id='contact-form-7-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/plugins/ctcore/assets/js/ct-front.js?ver=all' id='ct-front-js-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/themes/nimmo/assets/js/bootstrap.min.js?ver=4.0.0' id='bootstrap-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-includes/js/comment-reply.min.js?ver=5.7' id='comment-reply-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/themes/nimmo/assets/js/tilt.js?ver=all' id='tilt-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/themes/nimmo/assets/js/nice-select.min.js?ver=all' id='nice-select-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/themes/nimmo/assets/js/enscroll.js?ver=all' id='enscroll-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/themes/nimmo/assets/js/match-height-min.js?ver=1.0.0' id='match-height-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/themes/nimmo/assets/js/sidebar-scroll-fixed.js?ver=1.0.0' id='nimmo-sidebar-fixed-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/themes/nimmo/assets/js/magnific-popup.min.js?ver=1.0.0' id='magnific-popup-js'></script> <script type='text/javascript' id='nimmo-main-js-extra'> /* <![CDATA[ */ var main_data = {"ajax_url":"https:\/\/tech-chat.co.za\/wp-admin\/admin-ajax.php"}; /* ]]> */ </script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/themes/nimmo/assets/js/main.js?ver=1.1.9' id='nimmo-main-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/themes/nimmo/assets/js/smoothscroll.min.js?ver=all' id='smoothscroll-js'></script> <script type='text/javascript' src='https://www.google.com/recaptcha/api.js?render=6Le9dckUAAAAAPPFlpBelgGqNHoOA79MDzyruL6w&ver=3.0' id='google-recaptcha-js'></script> <script type='text/javascript' id='wpcf7-recaptcha-js-extra'> /* <![CDATA[ */ var wpcf7_recaptcha = {"sitekey":"6Le9dckUAAAAAPPFlpBelgGqNHoOA79MDzyruL6w","actions":{"homepage":"homepage","contactform":"contactform"}}; /* ]]> */ </script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/plugins/contact-form-7/modules/recaptcha/index.js?ver=5.4' id='wpcf7-recaptcha-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-includes/js/wp-embed.min.js?ver=5.7' id='wp-embed-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/plugins/ctcore/assets/js/one-page.js?ver=all' id='ct-one-page-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/plugins/js_composer/assets/js/dist/js_composer_front.min.js?ver=6.2.0' id='wpb_composer_front_js-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/plugins/js_composer/assets/lib/bower/imagesloaded/imagesloaded.pkgd.min.js?ver=6.2.0' id='vc_grid-js-imagesloaded-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/plugins/js_composer/assets/lib/bower/isotope/dist/isotope.pkgd.min.js?ver=6.2.0' id='isotope-js'></script> <script type='text/javascript' src='https://tech-chat.co.za/wp-content/plugins/js_composer/assets/lib/prettyphoto/js/jquery.prettyPhoto.min.js?ver=6.2.0' id='prettyphoto-js'></script> </body> </html> <!-- Page generated by LiteSpeed Cache 3.6.4 on 2021-04-10 05:54:55 -->