February 14, 2023
  • Engineering
  • Framework
  • Svelte

Blazing fast PWAs with SEO power using SvelteKit and Ionic

Tommertom

This is a guest post from Ionic Developer Expert Tom Gruintjes. While Ionic Framework does not officially support Svelte today, Tom created the ionic-svelte package, which uses Ionic Framework’s web components to create an out-of-the-box integration with Svelte. Continue reading to learn how you can use Svelte with Ionic Framework’s UI components.

In the world of web development, one year is almost a lifetime. So many things happen – APIs change with a blink of an eye, new frameworks come out, and for Svelte – SvelteKit 1.0 was finally released. SvelteKit is a so-called ‘meta-framework’. On top of Svelte, it provides tools, features, and opinions on how to organize your code for the client and the server. It also provides a router and all sorts of additional goodies to supercharge any web app.

SvelteKit projects can deliver blazing fast web-apps that support SEO out of the box. This blog aims to show you how to set up a fast, SEO-optimized landing page that allows users to install a PWA, optimized for performance by Lighthouse standards, utilizing SvelteKit and Ionic’s UI components made capable by ionic-svelte.

Step 1 – Let’s set up the Ionic SvelteKit project base project

Here we like to use the create command of the ionic-svelte repo as follows:

 npm create ionic-svelte-app@latest ionic-sveltekit-ssr-demo

You’ll see a new Ionic-Kit project spun up with the configurations and integrations all set:

Under the hood, this runs SvelteKit’s create command and spawns npm commands to get Ionic’s goodies in the project. Next, some configurations are done in svelte.config.js, tsconfig.json and package.json. As a plus – we add Capacitor integration dependencies, so our app can use its plugin ecosystem as PWA, as well as deploy as an Android/iOS app.

This command also sets our adapter configuration to static, meaning we will publish using static hosting (HTML, JS, CSS plus assets on Firebase Hosting, etc.). So, we opt out of managing API REST endpoints via SvelteKit in the same codebase. If we need that, switch to adapter auto.

Next – we set up Tailwind to support the styling of the landing page. Here you can follow the guide from the Tailwind site or you can use the instructions on GitHub. I used the latter and tweaked a few settings and references:

npx svelte-add@latest tailwindcss

We also set up the service worker. We deviate from the seemingly simple explanation of the Vite PWA Kit plugin – it did not work for us in combination with Vercel. And for those who don’t know – service workers also help compensate for low connectivity and flaky connections – and even offline experience for those app parts that can be used offline. How we did it – see app.html, vite.config.js (in the repo linked below) and run the following command:

npm i -D workbox-window vite-plugin-pwa

Steps 2 – Preparing routes and configure proper rendering patterns

Out of the box, we have Ionic loaded at the root route (see src/+layout.svelte) and ssr disabled for the whole app (see src/+layout.ts). We don’t want that, so we create a subroute for the PWA holding the Ionic UI goodies. And a subroute for the landing page:

  1. Create folder app under root route
  2. Copy +layout.* and +page.* from root to that folder
  3. Change path to theme variables in route/app/+layout.svelte (import '../../theme/variables.css';)

Then we configure ssr = true for the whole app, except the app routes:

In https://github.com/Tommertom/ionic-sveltekit-ssr-demo/blob/main/src/routes/%2Blayout.ts:

export const ssr = true

And in https://github.com/Tommertom/ionic-sveltekit-ssr-demo/blob/main/src/routes/app/%2Blayout.ts:

export const ssr = false

Next, we need to remove all Ionic references in the routes that have ssr = true:

  • +layout.svelte – remove everything except for the app.postcss (tailwind integration) and <slot/> – we don’t want the Ionic setup routine at root level
  • +page.svelte – remove all and replace with “Hello world!” – we’ll put the Landing page code here later

Now we have set up the proper web rendering patterns for each route: SSR for our landing page and SPA setup for the PWA, all in one code-base.

Step 3 – Creating the SEO powered landing page

Here we use a Tailwind+HTML starter template from the web. We opt for a static page, but of course you are free to use a more dynamic way using SvelteKit starters etc. CMS integration is also possible.

We have split the landing page parts in various components, so the landing page structure looks like this (see https://github.com/Tommertom/ionic-sveltekit-ssr-demo/blob/main/src/lib/components/Landingpage/Landing.svelte):

<script>
	import FAQ from '$lib/components/Landingpage/FAQ.svelte';
	import Features from '$lib/components/Landingpage/Features.svelte';
	import Footer from '$lib/components/Landingpage/Footer.svelte';
	import Hero from '$lib/components/Landingpage/Hero.svelte';
	import HowItWorks from '$lib/components/Landingpage/HowItWorks.svelte';
	import Pricing from '$lib/components/Landingpage/Pricing.svelte';
</script>

<!-- Hero -->
<Hero />

<!-- How it works -->
<HowItWorks />

<!-- Features -->
<Features />

<!-- Pricing -->
<Pricing />

<!-- FAQ  -->
<FAQ />

<!-- Footer -->
<Footer />

We added a simple button handler in <Hero> which captures the click for the CTA which navigates to the PWA.

In: https://github.com/Tommertom/ionic-sveltekit-ssr-demo/blob/main/src/lib/components/Landingpage/Hero.svelte:

<script>
	import { goto } from '$app/navigation';
	const goToApp = () => {
		goto('/app/install');
	};
</script>

and

<button on:click={goToApp}>Install app</button>

P.S. Do you see the amazing simple syntax of Svelte?!

And connect Landing.svelte component in routes/+page.svelte:

<script lang="ts">
	import Landing from '$lib/components/Landingpage/Landing.svelte';
</script>
<Landing/>

https://github.com/Tommertom/ionic-sveltekit-ssr-demo/blob/main/src/routes/%2Bpage.svelte

P.S. Again you can see the powerful and easy syntax of Svelte!

Now we can spin the app – npm run dev. Press o to open the browser so you can see the result so far: two working routes – / and /app. Do you notice how fast Vite boots?!

Step 4 – Configure the routing flows pending installation state of app

As we are serving two different appearances of our app on the same domain, we need to include some logic to handle the routing:

  • When installed as PWA – the landing page should be skipped
  • Otherwise, show the landing page (in later stage maybe even protect the PWA routes)

Here, I choose to do this handling on the client’s side for multiple reasons. We could optimize further into SSR – but we always need the window object to determine if we are in PWA state – so here we are paying a small price at one point. onMount is the lifecycle hook we will use to access the window object safely and the right path is navigated to.

<script lang="ts">
	import { goto } from '$app/navigation';
	import Landing from '$lib/components/Landingpage/Landing.svelte';
	import { onMount } from 'svelte';
	import { Capacitor } from '@capacitor/core';
	const isPWA = (win: Window): boolean =>
		!!(win.matchMedia?.('(display-mode: standalone)').matches || 
                (win.navigator as any).standalone);
	let showLanding = false;
	onMount(() => {
		console.log('Are we native?', Capacitor.isNativePlatform());
		if (Capacitor.isNativePlatform()) {
			console.log('Found native shell, redirecting');
			goto('/app/login');
			return;
		}
		if (isPWA(window)) {
			console.log('In PWA - on wrong route - redirecting');
			goto('/app/splash');
		} else showLanding = !isPWA(window);
	});
</script>
{#if showLanding}
	<Landing />
{/if}

This logic uses the code from Ionic’s platform API. We cannot import this directly from ionic-svelte as we will get a 500 error. Next, to avoid flicker going to the PWA in Capacitor (or a wrong path in PWA), we also need to conditionally render the Landing page. This weakens the “full SSR” claim as we do our checks after the client is fully mounted (aka AfterViewInit in Angular)– Again, a small price, and in our case, no big deal.

To optimize further we set start_url: "/app/splash" in the PWA manifest. https://github.com/Tommertom/ionic-sveltekit-ssr-demo/blob/main/vite.config.js

Step 5 – Performance boosting the the SPA

We need to assure the SPA gets its maximum power by pre-loading and pre-caching it. We can do this while the user is looking at the landing page, signing up and/or reading the T&C etc. (not implemented in the repo). For this, we:

  • Use the service worker to run from the root route – even though we really need it at /app route
  • Configure SvelteKit to prefetch all code once the SPA is invoked – basically prefetching all routes, instead of lazy loading. We cannot do this in the landing page, as it will load Ionic in SSR (500 error)

The prefetching happens through a call to preloadCode() when not in dev mode. Putting this in +layout.svelte means – the moment any route in the PWA is called, it will prefetch the code of the whole app. We don’t want this in dev-mode.

https://github.com/Tommertom/ionic-sveltekit-ssr-demo/blob/main/src/routes/app/%2Blayout.svelte

// Prefetch all code when not in dev
if (!dev) preloadCode();

And when this code is in the service worker cache, it will get this code from the cache – creating a huge performance boost beyond the first run. Again, we can only run preloadCode() in the Ionic enabled routes. Otherwise, we get a 500 error.

Step 6 – Adding some install, signup, T&C and splash goodies, etc…

Just for fun I added a splash page for the PWA, the install route and a mock-login route. The final repo has four routes:

  • /route – main entry point
  • /app – should lead to redirect to splash
  • /install – showing the installation UI and logic
  • /splash – splash page (optional)
  • /login – dummy login/signup page

Some important points to note:

  • For installation on Android phones, we experienced issues with the Samsung browser. So we need to cover for that and instruct the user to use a different browser for installing the app. Also, we must think through carefully instructing users how to launch the PWA (which could be a full blog on its own if you ask me)!
  • The iOS installation routine needs to be done manually
  • In our project, we needed Firebase-push notifications through the web SDK. This requires an additional service worker to be installed with a designated name. We placed that one under route scope /app and initialized it manually via code in app.html. A bit of a pain, but it works.

End result – pretty neat lighthouse scores and good performance on test devices

It already became very apparent that this setup performs amazingly fast on mobile devices with limited connectivity, and the small bundle size of SvelteKit build files really helps a lot.

Just check out the demo app and see how fast it loads the landing page – and then do the install routine (Android Chrome, Chrome on Windows/Mac or manual install on Safari/iOS).

Here is the app – https://ionic-svelte-ssr.web.app and Vercel: https://ionic-sveltekit-ssr-demo.vercel.app/

See here the PWA and SEO lighthouse scores:

Again – no strong efforts on the SEO scores – and we get sufficient hints from Lighthouse to improve it. The performance is what matters here.

And WebPageTest using mobile and high bandwidth:

Low spec phone testing with 3G connection isn’t bad either:

We were quite happy with these scores – a big improvement while using a simple tech stack. This will pay off as we get users on-board, and there are still opportunities to further optimize.

Of course, this app is quite basic. Then again, how big do you want to make your landing pages? By using lazy loading and service workers, it becomes quite easy to optimize the SPA.

Bonus – adding Capacitor

In the repo I added a condition to check if the app actually runs in a Capacitor container (iOS/Android), and then properly directs it to the PWA while skipping the Web Splash as we want the native Splash in these cases.

In https://github.com/Tommertom/ionic-sveltekit-ssr-demo/blob/main/src/routes/%2Bpage.svelte:

onMount(() => {
		console.log('Are we native?', Capacitor.isNativePlatform());
		if (Capacitor.isNativePlatform()) {
			console.log('Found native shell, redirecting');
			goto('/app/login');
			return;
		}

		if (isPWA(window)) {
			console.log('In PWA - on wrong route - redirecting');
			goto('/app/splash');
		} else showLanding = !isPWA(window);
	});

Easy!!

Want to know more?

  • Svelte.dev, Kit.svelte.dev and Learn.svelte.dev are real great resources to learn more about SvelteKit – warning: if you come from SPA background – the Kit part is a bit steep
  • Ionic-svelte and the NPM package (link) – if you like to try Ionic UI in SvelteKit
  • Ionic-svelte discord channel to address all your questions and comments
  • Rendering patterns to maximize performance – see Fireship’s blog on rendering patterns – https://www.youtube.com/watch?v=Dkx5ydvtpCA

To continue to improve this code, please share your findings as issues on the main repo: https://github.com/Tommertom/ionic-sveltekit-ssr-demo

For check out the live demo, visit https://ionic-svelte-ssr.web.app (Firebase-based hosted) or https://ionic-sveltekit-ssr-demo.vercel.app (Vercel-hosted).

About the author

Tom works as project and innovation manager at a Dutch retail bank working in various emerging markets. He is passionate about technology and more specifically front-end software development. Tom got started with Ionic and Angular 2+ back in 2016 and is now focused on Svelte and Ionic. Tom has launched the ionic-svelte package, which provides out-of-the-box integration with Ionic and Svelte – still work in progress. Just run npm create ionic-svelte-app@latest to spin your own Ionic Svelte project!

Tom also launched a showcase app for Ionic elements to run from your browser and phone, with an easy source-code viewer (Vue, Angular, Vanilla, Stencil, Svelte, and React).


Tommertom