Improving SvelteKit passkey authentication performance

Toby Hobson - Oct 2 - - Dev Community

This tutorial forms the third part of my SvelteKit series. If you haven't already done so, please read the first and second tutorials. In this instalment, I'll explain how we can improve the actual and perceived performance of our authentication operations.

What's the goal?

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 (this does need to be fast).

Nevertheless, we want to offer users a decent experience.

We need to improve the actual 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 by our passkeys, we risk introducing browser incompatibilities and security vulnerabilities. In most cases the tradeoff isn't worth it.

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

Pre-connect to the backend

When you first register or authenticate with a passkey, the Passlock library will invoke an AWS lambda. This may 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()
})
Enter fullscreen mode Exit fullscreen mode

This will fire up a Lambda and perform some initialization tasks, ready for the incoming register or authenticate call.

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

Update the login template

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

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Tip: 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 the Passlock library makes it easy to add social login to your SvelteKit apps.

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

. . .
Terabox Video Player