<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://web.dev/</id>
  <title>Scott Friesen on web.dev</title>
  <updated>2026-04-15T23:21:06Z</updated>
  <author>
    <name>Scott Friesen</name>
  </author>
  <link href="https://web.dev/authors/scottfriesen/feed.xml" rel="self"/>
  <link href="https://web.dev/"/>
  <icon>https://web-dev.imgix.net/image/admin/D4T7yi0PpYUsSRf38KKc.jpg?auto=format</icon>
  <logo>https://web.dev/images/shared/rss-banner.png</logo>
  <subtitle>Our latest news, updates, and stories by Scott Friesen.</subtitle>
  
  
  <entry>
    <title>How ZDF created a video PWA with offline and dark mode</title>
    <link href="https://web.dev/zdf/"/>
    <updated>2020-10-07T00:00:00Z</updated>
    <id>https://web.dev/zdf/</id>
    <content type="html" mode="escaped">&lt;p&gt;When broadcaster ZDF was considering redesigning their frontend technology
stack, they decided to take a closer look at &lt;a href=&quot;https://web.dev/pwa/&quot;&gt;Progressive Web Apps&lt;/a&gt; for their
streaming site &lt;a href=&quot;https://pwa.zdf.de/&quot; rel=&quot;noopener&quot;&gt;ZDFmediathek&lt;/a&gt;. Development agency
&lt;a href=&quot;https://www.cellular.de/&quot; rel=&quot;noopener&quot;&gt;Cellular&lt;/a&gt; took on the challenge to build a web-based
experience that is on par with ZDF&#39;s platform-specific iOS and Android apps. The
PWA offers installability, offline video playback, transition animations, and a
dark mode.&lt;/p&gt;
&lt;h2 id=&quot;adding-a-service-worker&quot;&gt;Adding a service worker &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/zdf/#adding-a-service-worker&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A key feature of a PWA is offline support. For ZDF most of the heavy lifting is done by
&lt;a href=&quot;https://web.dev/workbox/&quot;&gt;Workbox&lt;/a&gt;, a set of libraries
and Node modules that make it easy to support different caching strategies. The
ZDF PWA is built with TypeScript and React, so it uses the Workbox library
already built into
&lt;a href=&quot;https://reactjs.org/docs/create-a-new-react-app.html&quot; rel=&quot;noopener&quot;&gt;create-react-app&lt;/a&gt; to
precache static assets. This lets the application focus on making the dynamic
content available offline, in this case the videos and their metadata.&lt;/p&gt;
&lt;p&gt;The basic idea is quite simple: fetch the video and store it as a blob in
IndexedDB. Then during playback, listen for online/offline events, and switch to
the downloaded version when the device goes offline.&lt;/p&gt;
&lt;p&gt;Unfortunately things turned out to be a little more complex. One of the project
requirements was to use the official ZDF web player which doesn&#39;t provide any
offline support. The player takes a content ID as input, talks to the ZDF API,
and plays back the associated video.&lt;/p&gt;
&lt;p&gt;This is where one of the web&#39;s most powerful features comes to the rescue:
&lt;a href=&quot;https://web.dev/service-worker-mindset/&quot;&gt;service workers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The service worker can intercept the various requests done by the player and
respond with the data from IndexedDB. This transparently adds offline
capabilities without having to change a single line of the player&#39;s code.&lt;/p&gt;
&lt;p&gt;Since offline videos tend to be quite large, a big question is how many of them
can actually be stored on a device. With the help of the &lt;a href=&quot;https://web.dev/storage-for-the-web/#how-much&quot;&gt;StorageManager
API&lt;/a&gt; the app can estimate the
available space and inform the user when there is insufficient space before even
starting the download. Unfortunately Safari isn&#39;t on the list of browsers
implementing this API and at the time of writing there wasn&#39;t much up-to-date
information about how other browsers applied quotas. Therefore, the team wrote a
&lt;a href=&quot;https://cellular.github.io/quota&quot; rel=&quot;noopener&quot;&gt;small utility&lt;/a&gt; to test the behavior on
various devices. By now a &lt;a href=&quot;https://web.dev/storage-for-the-web/&quot;&gt;comprehensive
article&lt;/a&gt; exists that sums up all the
details.&lt;/p&gt;
&lt;h2 id=&quot;adding-a-custom-install-prompt&quot;&gt;Adding a custom install prompt &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/zdf/#adding-a-custom-install-prompt&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The ZDF PWA offers a custom in-app installation flow and prompts users to
install the app as soon as they want to download their first video. This is a
good point in time to prompt for install because the user has expressed a clear intention to
use the app offline.&lt;/p&gt;
&lt;figure&gt;
  &lt;div class=&quot;switcher&quot;&gt;
    &lt;img alt=&quot;Custom invitation to install.&quot; decoding=&quot;async&quot; height=&quot;1595&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/sj4J2JMlYdgf4BrhaRsT.jpg?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
    &lt;img alt=&quot;Custom install prompt being triggered when downloading a video for offline consumption.&quot; decoding=&quot;async&quot; height=&quot;1595&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FT4Xt5xpjCp57C8BwLtn.jpg?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
  &lt;/div&gt;
  &lt;figcaption&gt;Custom install prompt being triggered when downloading a video for offline consumption.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;building-an-offline-page-to-access-downloads&quot;&gt;Building an offline page to access downloads &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/zdf/#building-an-offline-page-to-access-downloads&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When the device is not connected to the internet and the user navigates to a
page that is not available in offline mode, a special page is shown instead that
lists all videos that have previously been downloaded or (in case no content has
been downloaded yet) a short explanation of the offline feature.&lt;/p&gt;
&lt;figure&gt;
  &lt;div class=&quot;switcher&quot;&gt;
    &lt;img alt=&quot;Offline page showing all content available for watching offline.&quot; decoding=&quot;async&quot; height=&quot;1418&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/FcWDhtuSSpHg04krFqUD.png?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
    &lt;img alt=&quot;Offline page showing that no content is available for watching offline.&quot; decoding=&quot;async&quot; height=&quot;1423&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/tcFciHGuF3MxnTr1y5ue01OGLBn2/PUvFyyaVfhh7PFyXDwCo.png?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
  &lt;/div&gt;
  &lt;figcaption&gt;Offline page showing all content available for watching offline.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;using-frame-loading-rate-for-adaptive-features&quot;&gt;Using frame loading rate for adaptive features &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/zdf/#using-frame-loading-rate-for-adaptive-features&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To offer a rich user experience the ZDF PWA includes some subtle transitions
that happen when the user scrolls or navigates. On low-end devices such
animations usually have the opposite effect and make the app feel sluggish and
less responsive if they don&#39;t run at 60 frames per second. To take this into
account the app measures the actual frame rate via &lt;code&gt;requestAnimationFrame()&lt;/code&gt; while
the application loads and disables all animations when the value drops below a
certain threshold.&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; frameRate &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resolve&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; lastTick&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; samples &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;measure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; tick &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lastTick&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; samples&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tick &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; lastTick&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    lastTick &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; tick&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;samples&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;requestAnimationFrame&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;measure&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; avg &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; samples&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; samples&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fps &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; avg&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;      &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fps&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token function&quot;&gt;measure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;Even if this measurement provides only a rough indication of the device&#39;s
performance and varies on each load, it was still a good basis for
decision-making. It&#39;s worth mentioning that depending on the use case there are
&lt;a href=&quot;https://web.dev/adaptive-loading-with-service-workers/&quot;&gt;other techniques for adaptive loading&lt;/a&gt;
that developers can implement. One great advantage of this approach is that it
is available on all platforms.&lt;/p&gt;
&lt;h2 id=&quot;dark-mode&quot;&gt;Dark mode &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/zdf/#dark-mode&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A popular feature for modern mobile experiences is
&lt;a href=&quot;https://web.dev/prefers-color-scheme/&quot;&gt;dark mode&lt;/a&gt;.
Especially when
watching videos in low ambient light many people prefer a dimmed UI. The ZDF PWA
not only provides a switch that allows users to toggle between a light and a
dark theme, it also reacts to changes of the OS-wide color preferences. This way
the app will automatically change its appearance on devices that have set up a
schedule to change the theme base on the time of day.&lt;/p&gt;
&lt;h2 id=&quot;results&quot;&gt;Results &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/zdf/#results&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The new progressive web app was silently launched as a public beta in March 2020
and has received a lot of positive feedback since then. While the beta phase
continues, the PWA still runs under its own temporary domain. Even though the
PWA wasn&#39;t publicly promoted there is a steadily growing number of users. Many
of these are from the Microsoft Store which allows Windows 10 users to discover
PWAs and install them like platform-specific apps.&lt;/p&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What&#39;s next? &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/zdf/#whats-next&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;ZDF plans to continue adding features to their PWA, including login for
personalization, cross-device and platform viewing, and push notifications.&lt;/p&gt;
</content>
    <author>
      <name>Scott Friesen</name>
    </author><author>
      <name>Martin Schierle</name>
    </author>
  </entry>
</feed>
