We're now live! Signup now

SvelteKit Passkey Performance

Passkeys Sveltekit

If you've followed the previous installments in this series, you'll have built a SvelteKit app with passkey authentication, session management and authorization. It's functional, but slow and clunky. Let's improve things.

This tutorial forms the third part of our SvelteKit series. If you haven’t already done so, please read the first and second tutorials. In this instalment, we’ll improve actual and perceived performance.

Perceived vs actual performance

We’re not aiming for millisecond responses. Passkey authentication should happen relatively infrequently. Once the user has an active session, session authentication and request authorization takes over. Nevertheless, we want to offer users a decent experience.

We need to improve the actual performance, and perceived performance. Perceived performance is all about giving the user feedback that things are happening.

Performance improvements

The app is a bit slow and clunky. There are a few reasons for this:

  1. We’re running in dev mode
  2. We’re dealing with public key cryptography which is slow
  3. Being serverless, the Passlock backend spins up for “cold” requests

Ultimately we’ll be running in production mode with the inherent optimizations. Whilst we can optimize the choice of ciphers used for our passkeys, we risk introducing browser incompatabilities and security vulnerabilities. In most cases the tradeoff isn’t worth it.

That leaves us with the last area for improvement - optimizing for serverless.

Pre-connect to the Passlock backend

When you first register or authenticate a passkey, Passlock will invoke an AWS lambda. This will most likely result in a “cold start” which isn’t ideal. However, we can work around this using Svelte’s onMount feature:

Update src/routes/register/+page.svelte:

// src/routes/register/+page.svelte
import { onMount } from 'svelte'

const passlock = new Passlock({ ... })

onMount(async () => {
  await passlock.preConnect()
})
TIP
We're basically firing up a serverless Lambda when the page loads, which sits there waiting for the registerPasskey call.

Now do the same for the login template at src/routes/login/+page.svelte.

NOTE
You should see browser dev tool console entries like this:
timestamp=xxx level=INFO fiber=#30 message="Pre-connecting to RPC endpoint"

Provide feedback

Perceived performance is just as important as actual performance. Whilst waiting for a network request, we want to provide some feedback to the user, so they know something is happening. There are multiple ways of doing this, but we’ll go for a simple spinner.

Update src/routes/register/+page.svelte:

<script lang="ts">
  import { applyAction, enhance } from '$app/forms'
  import { Passlock, PasslockError } from '@passlock/sveltekit'
  import type { SubmitFunction } from './$types'
  import { goto } from '$app/navigation'
  import Spinner from '$lib/Spinner.svelte'
  import { onMount } from 'svelte'

  import {
    PUBLIC_PASSLOCK_TENANCY_ID,
    PUBLIC_PASSLOCK_CLIENT_ID
  } from '$env/static/public'

  const passlock = new Passlock({
    tenancyId: PUBLIC_PASSLOCK_TENANCY_ID,
    clientId: PUBLIC_PASSLOCK_CLIENT_ID,
    endpoint: 'https://okbq1o3xde.execute-api.eu-west-2.amazonaws.com'
  })

  let requestPending = false

  const onSubmit: SubmitFunction = async ({ formData, cancel }) => {
    // show the spinner when the request is made
    requestPending = true
    const email = formData.get('email') as string
    const givenName = formData.get('givenName') as string
    const familyName = formData.get('familyName') as string

    const result = await passlock.registerPasskey({
      email,
      givenName,
      familyName
    })

    if (PasslockError.isError(result)) {
      // if we can't register a passkey, abort
      requestPending = false
      cancel()
      alert(result.message)
    } else {
      formData.set('token', result.token)
    }

    return async ({ result }) => {
      // form action completed so hide the spinner
      requestPending = false
      if (result.type === 'redirect') {
        goto(result.location)
      } else {
        await applyAction(result)
      }
    }
  }

  onMount(async () => {
    await passlock.preConnect()
  })
</script>

<form method="post" use:enhance="{onSubmit}">
  Email:
  <input type="text" name="email" />
  <br />
  First name:
  <input type="text" name="givenName" />
  <br />
  Last name:
  <input type="text" name="familyName" />
  <br />
  <input type="hidden" name="token" />
  <button type="submit">Register</button>

  {#if requestPending}
  <Spinner />
  {/if}
</form>

While the form submission is in progress we set requestPending to true and therefore display the spinner. If the passkey registration fails we cancel the form submission and hide the spinner. Finally once we get a response back from the form action we hide the spinner.

NOTE
Spinner.svelte is just an SVG spinner wrapped as a Svelte component. You're free to use anything here, even a simple "loading..." message.

Add a login spinner

Now do the same for the src/routes/login route:

<!-- src/routes/login/+page.svelte -->
<script lang="ts">
  import { applyAction, enhance } from '$app/forms'
  import { Passlock, PasslockError } from '@passlock/sveltekit'
  import type { SubmitFunction } from './$types'
  import Spinner from '$lib/Spinner.svelte'
  import { onMount } from 'svelte'

  import {
    PUBLIC_PASSLOCK_TENANCY_ID,
    PUBLIC_PASSLOCK_CLIENT_ID,
    PUBLIC_PASSLOCK_ENDPOINT
  } from '$env/static/public'
  import { goto } from '$app/navigation'

  const passlock = new Passlock({
    tenancyId: PUBLIC_PASSLOCK_TENANCY_ID,
    clientId: PUBLIC_PASSLOCK_CLIENT_ID,
    endpoint: PUBLIC_PASSLOCK_ENDPOINT
  })

  let requestPending = false

  const onSubmit: SubmitFunction = async ({ cancel, formData }) => {
    // show the spinner when the request is made
    requestPending = true
    const email = formData.get('email') as string

    const user = await passlock.authenticatePasskey({ email })

    if (!PasslockError.isError(user)) {
      // if we can't login using a passkey, abort
      requestPending = false
      formData.set('token', user.token)
    } else {
      cancel() // prevent form submission
      alert(user.message)
    }

    return async ({ result }) => {
      // form action completed so hide the spinner
      requestPending = false
      if (result.type === 'redirect') {
        goto(result.location)
      } else {
        await applyAction(result)
      }
    }
  }

  onMount(async () => {
    await passlock.preConnect()
  })
</script>

<form method="post" use:enhance="{onSubmit}">
  Email:
  <input type="text" name="email" />
  <br />
  <button type="submit">Login</button>

  {#if requestPending}
  <Spinner />
  {/if}
</form>

Summary

We’ve improved perceived performance by a slight of hand. We’re pre-connecting to the Passlock serverless backend when the page loads and displaying a loading spinner whilst requests are pending.

Supporting code
The final SvelteKit app is available in a GitHub repo. Clone the repo and check out the tutorial/pt-3 tag.

Next steps

Passkeys are a great alternative to passwords, but many users still prefer to sign in to apps with a social account. Fortunately Passlock allows you to add social login to your SvelteKit apps.

Social login is the next tutorial in this SvelteKit authentication series…

Toby Hobson

Toby Hobson

Founder

Passkeys

SvelteKit + Passkeys + Lucia

In this tutorial you'll build on your previous work to integrate passkeys with Lucia users and sessions. You'll handle session creation, expiry and invalidation and protect your routes using SvelteKit hooks.

Passkeys

SvelteKit + Passkeys

In this tutorial you'll learn how to add passkey authentication to your SvelteKit apps. You'll register a passkey and use it to login. In subsequent tutorials I'll show you how to add session management, social login and more.

Passkeys

Passkey browser support in 2024

All the major browsers now support passkeys, however biometric support is often limited to those browsers with tight platform integration e.g. Safari on iOS and Chrome on Android.

Passkeys

Two factor authentication using passkeys

Passkeys enable two factor authentication (including biometrics). Users can even use a biometric enabled device e.g. iPhone FaceID to authenticate against a device lacking this capability e.g. a desktop.

Want product news and updates?

Sign up for our newsletter

We care about your data. Read our privacy policy .