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:
- We're running in dev mode
- We're dealing with public key cryptography which is slow
- 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()
})
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>
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.
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...