24 Time Saving Tips for Nuxt 3

Nuxt has so many amazing features. This article is a compilation of 24 tips to help you save time and write better Nuxt apps.

Michael Thiessen
Nuxt 3

Mastering Nuxt 3 course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view the Nuxt 3 course

We know that Nuxt is a fantastic tool.

But it has so many amazing features that it’s hard to keep track of them all.

That’s why I’ve compiled this giant list of 24 Nuxt tips for you — use them to save time and write better Nuxt apps.

We cover a lot of topics here, including:

  • When to use /assets vs. /public directory
  • Using runtimeConfig vs. app.config
  • Understanding how Universal rendering works (and how it’s different from SPA and SSR)
  • A utility to make your own NuxtLink components that no one is talking about
  • Adding a basic cache to your data fetching — since Nuxt doesn’t do this by default

Of course, there is so much more!

Which tip is your favourite?

1. Lazy Loaded Components

Not all your components need to be loaded immediately.

With Nuxt we can defer loading by adding Lazy as a prefix.

Nuxt does all the heavy-lifting for us!

<!-- Loads as soon as possible -->
<Modal v-if="showModal" />

<!-- Only loads when showModal = true -->
<LazyModal v-if="showModal" />

2. Prerender Specific Routes with Nitro

By configuring Nitro directly, we can have only some routes pre-rendered.

Every other route will use hybrid rendering, like normal:

export default defineNuxtConfig({
  nitro: {
    prerender: {
      routes: ['/about', '/blog'],
    },
  },
});

3. Use the built-in key-value storage

In Nuxt we can use a simple, but powerful, built-in key-value storage:

const storage = useStorage();
const key = `session:token`;

// Save the value
await storage.setItem(key, sessionToken);

// ...and retrieve it in a different API call
const token = await storage.getItem(key);

4. Auto-imports

By taking advantage of auto-imports, we can quickly access the route and user information without needing to manually import them.

This helps make our code more organized, efficient, and readable.

<script setup>
// We can use the imported route and user without having to import it ourselves
const { path, params } = useRoute();
const userData = useCustomComposable();
</script>

5. Control the head script reactively

Nuxt 3 allows developers to reactively control the <head> of their application with the useHead composable.

This lets you use a dynamic titleTemplate or dynamic script tags:

useHead({
  titleTemplate: (title) => `${title} | My Website`,
  script: [
    {
       src: 'https://www.scripts.com/some/other/script.js',
       body: true
     }
  ]
})

6. Get route information quickly

The useRoute composable in Nuxt.js 3 makes it easy to grab info from the route, and also query parameters.

Here's an example:

const route = useRoute();

console.log(route.fullPath);
// https://www.website.com/?search=hello%20there

console.log(route.query.search);
// there

7. Handle client-side errors with ease

Using NuxtErrorBoundary components around distinct chunks of functionality in your app allows you to handle a group of errors together, providing better UX.

This lets you contain errors in your app and handle them in specific ways instead of using a generic error page.

<NuxtErrorBoundary>
  <NuxtPage />
  <template #error="{ error }">
    <div>
      <p>
        Oh no, something broke when loading the lesson!
        <code>{{ error }}</code>
      </p>
      <p>
        <button
          class="hover:cursor-pointer"
          @click="clearError(error)"
        >
          Go to the first lesson
        </button>
      </p>
    </div>
  </template>
</NuxtErrorBoundary>

8. Nested Routes (aka Child Routes)

Nuxt uses the NuxtPage component to render a page from your app’s pages/ directory.

Adding nested NuxtPage components lets you match to multiple pages in your folder hierarchy, allowing for more flexible routing:

// pages/one/two/three.vue
<template>
  <!-- The nested NuxtPage renders child routes -->
  <NuxtPage />
</template>

Nested routes in Nuxt can be thought of as a folder hierarchy, which can help organize and structure your application.

For example, the route /one/two/three is equivalent to this folder structure:

one/
- two/
- - three/

8. /assets vs. /public — how do you decide?

Nuxt 3 offers two options for managing assets in your web app:

  • ~/assets folder
  • ~/public folder

Choose assets folder if the assets need processing, change often, and don’t require a specific filename.

Otherwise, use the public directory.

// Using ~/assets
<script setup>
import image from '~/assets/image.png';
</script>
<template>
  <img :src="image" />
</template>

// Using ~/public
<template>
  <img src="/image.png" />
</template>

9. Using the /assets directory

The ~/assets folder in Nuxt 3 is ideal for assets that need processing.

eg. images, stylesheets, icons, and fonts.

When you import an asset from this folder, the bundler processes the file, generates a new filename with a hash, and replaces the import with the new filename.

import image from '~/assets/image.png';
import style from '~/assets/style.css';

The ~/assets folder has the added benefit of catching missing assets during the build process.

If an asset is missing, you'll get a build error, which helps you maintain your app's integrity.

This won't happen with assets in the ~/public folder since they aren't processed.

// Importing from ~/assets will catch missing assets
import missingImage from '~/assets/missing-image.png'; // Build error

// Referencing from ~/public won't catch missing assets
// In your template: <img src="/missing-image.png" /> // No build error

10. Using the /public directory

The ~/public folder is great for assets that don't need processing and should be served directly from the root of your app.

Files in this folder are not modified and are copied directly to the build output.

// Accessing files from ~/public
yourwebsite.com/robots.txt
yourwebsite.com/favicon.ico
yourwebsite.com/sitemap.xml

You can also encapsulate a lot of these different configurations into your own link components if you want, using defineNuxtLink:

// ~/components/MyLink.ts

// Only colour prefetched links during development
export default defineNuxtLink({
    componentName: 'MyLink',
  prefetchedClass: process.env.NODE_ENV === 'development'
        ? 'prefetched'
        : undefined,
});

Here we create our own MyLink component that will set a special class on prefetched links, but only during development.

You can do a lot more with defineNuxtLink:

defineNuxtLink({
  componentName?: string;
  externalRelAttribute?: string;
  activeClass?: string;
  exactActiveClass?: string;
  prefetchedClass?: string;
  trailingSlash?: 'append' | 'remove'
}) => Component

If you want to learn more, I recommend going straight to the docs, or to the source code itself.

With internal links, NuxtLink can check to see if it’s in the viewport in order so it can preload data before you even need it:

<NuxtLink to="/articles" prefetch>Articles</NuxtLink>

This behaviour is on by default, so you don’t even need to worry about it most of the time. But the prop is helpful if you need to disable it for some reason:

<NuxtLink to="/articles" :prefetch="false">Articles</NuxtLink>

We can also do the same thing with noPrefetch:

<NuxtLink to="/articles" no-prefetch>Articles</NuxtLink>

If the route has been prefetched, Nuxt will set a prefetchedClass on the link:

<NuxtLink
    to="/articles"
    prefetched-class="prefetched"
>
    Articles
</NuxtLink>

This can be very useful during debugging, but probably not as useful to your end users!

Did you know that NuxtLink can also handle external links?

It will automatically add noopener and noreferrer for security.

It auto-detects external links, but there’s also an external prop if needed.

<NuxtLink to="www.masteringnuxt.com" external>
  Mastering Nuxt 3!
</NuxtLink>

For external links, it will automatically adding in noopener and noreferrer attributes for security:

<!-- Using an anchor tag -->
<a href="www.masteringnuxt.com" rel="noopener noreferrer">Mastering Nuxt 3</a>

<!-- Replace with NuxtLink -->
<NuxtLink to="www.masteringnuxt.com">Mastering Nuxt 3</NuxtLink>

14. Data-fetching and the key parameter

The key parameter is an optional argument you can provide to the useAsyncData and useFetch composables.

Internally, the key parameter is used to create a unique identifier for the fetched data. This helps optimize the performance of your application by reducing unnecessary data fetching on the client if the data has already been fetched on the server.

Imagine you're building a music production app where users can select a project and view the associated tracks. You can use the key parameter to ensure that the tracks are updated whenever the selected project changes:

<template>
  <div>
    <select v-model="selectedProject">
      <option v-for="project in projects" :value="project.id" :key="project.id">
        {{ project.name }}
      </option>
    </select>

    <div v-if="pending">Loading tracks...</div>
    <div v-else-if="error">{{ error.message }}</div>
    <div v-else>
      <ul>
        <li v-for="track in tracks" :key="track.id">{{ track.name }}</li>
      </ul>
    </div>
  </div>
</template>
const selectedProject = ref(1)
const projects = [
  { id: 1, name: 'Project A' },
  { id: 2, name: 'Project B' },
  { id: 3, name: 'Project C' },
]

const { data: tracks, pending, error } = useAsyncData(
  'tracks',
  () => fetch(`https://api.example.com/projects/${selectedProject.value}/tracks`)
)

Here, the key parameter is tracks.

When the data is fetched on the server and passed along with the client bundle, the client knows it doesn’t need to re-fetch that data since it’s already been fetched.

If you don’t provide a key, Nuxt will automatically create one for you based on the line and file of where it’s used.

15. Easy auth with Supabase + Nuxt

To log in a user with Github as the OAuth provider, we can use the Supabase auth client:

const supabase = useSupabaseAuthClient();

const login = async () => {
  const { error } = await supabase.auth.signInWithOAuth({
    provider: 'github',
  });

  if (error) {
    console.error(error);
    // Handle error, e.g., show a notification to the user
  }
};

There’s really nothing more to it!

16. The useAsyncData composable

The useAsyncData composable is a powerful composable provided by Nuxt that allows you to fetch data asynchronously in your components.

This is particularly useful when you need to fetch data from an API or perform other asynchronous tasks before rendering your component.

Here's an example of how you might use useAsyncData in a music production app to fetch a list of instruments:

<template>
  <div>
    <h1>Available Instruments</h1>
    <ul v-if="!pending && !error">
      <li v-for="instrument in instruments" :key="instrument.id">
        {{ instrument.name }}
      </li>
    </ul>
    <p v-if="pending">Loading...</p>
    <p v-if="error">Error: {{ error.message }}</p>
  </div>
</template>
const { data: instruments, pending, error } = useAsyncData(
  'instruments',
  () => fetch('https://api.example.com/instruments')
)

In this example, we're using useAsyncData to fetch the list of instruments and assign the result to a reactive instruments variable.

We also have access to pending and error properties, which can be used to display loading and error states in our template.

You can check out the documentation for more information.

17. The useFetch composable

This is another composable provided by Nuxt that simplifies data fetching in your components.

It's a wrapper around useAsyncData and provides some additional features, such as automatic key generation based on the URL and fetch options.

Here's an example of how you might use useFetch in a music production app to fetch a list of tracks for a specific project:

<template>
  <div>
    <h1>Project Tracks</h1>
    <ul v-if="!pending && !error">
      <li v-for="track in tracks" :key="track.id">
        {{ track.name }}
      </li>
    </ul>
    <p v-if="pending">Loading...</p>
    <p v-if="error">Error: {{ error.message }}</p>
  </div>
</template>
const projectId = 1
const { data: tracks, pending, error } = useFetch(
  `https://api.example.com/projects/${projectId}/tracks`
)

In the example above, we're checking if pending is true and displaying a loading message if so. Additionally, we're checking if there's an error and displaying the error message if one occurs.

To ensure your component updates when the project ID changes, you can pass in the projectId as a ref instead:

const projectId = ref(1)
const { data: tracks, pending, error } = useFetch(
  () => `https://api.example.com/projects/${projectId.value}/tracks`
)

This way, if the projectId value changes, the URL will update accordingly and the data will be fetched again.

If you want, you can check out the documentation for more information.

18. Compressing images with VSharp

There are a few different Vite plugins for compressing images, but for this tip we’ll use [vsharp](https://github.com/jw-12138/vite-plugin-vsharp).

To install, run pnpm add -D vite-plugin-vsharp.

Then, we update our configuration to look something like this:

import vsharp from 'vite-plugin-vsharp';

export default defineNuxtConfig({
    // 👇 Add vsharp to the vite plugins
  vite: {
    plugins: [vsharp()],
  },

  // The rest of your Nuxt config
  runtimeConfig: {
        // ...
  },
  modules: [
        // ...
  ],
});

That’s it!

As long as we make sure our images are in /assets and then imported into our components, they’ll be compressed during the build.

That’s a big win for a small config change!

19. Add basic caching to your fetch

Here’s what our useFetchWithCache composable looks like:

// ~/composables/useFetchWithCache.ts

import { StorageSerializers } from '@vueuse/core';

export default async <T>(url: string) => {
  // Use sessionStorage to cache data
  const cached = useSessionStorage<T>(url, null, {
    serializer: StorageSerializers.object,
  });

  if (!cached.value) {
    const { data, error } = await useFetch<T>(url);

    if (error.value) {
      throw createError({
        ...error.value,
        statusMessage: `Could not fetch data from ${url}`,
      });
    }

    // Update the cache
    cached.value = data.value as T;
  } else {
    console.log(`Getting value from cache for ${url}`);
  }

  return cached;
};

Here are two examples for how this might be used:

// Get all users
const user = useFetchWithCache<User[]>('/api/users') 

// Or fetch the settings
const settings = useFetchWithCache<Settings>('/api/settings')

Read the full tutorial to see how this all works in detail.

20. Understand the Benefits of Universal Rendering

Nuxt offers a unique solution to the limitations of SPAs and SSRs by combining their strengths. This approach, called Universal Rendering, provides the best of both worlds.

Lightning Fast First Page Load

On the first page load, Nuxt uses SSR to deliver a fast initial experience.

It processes the request on the server and sends back the HTML and other necessary files, similar to a traditional SSR app. This ensures that users are not kept waiting, which is particularly important for maintaining user engagement and optimizing search engine rankings.

Seamless Transition to SPA

However, Nuxt doesn't stop at the initial SSR.

It also loads the entire app as a SPA, so everything after the first page load is extremely quick. Once the initial page is loaded, Nuxt switches to SPA mode, allowing users to navigate within the app without needing to make round trips to the server.

Performance Enhancements and Optimizations

It's worth noting that Nuxt goes beyond simply combining SSR and SPA approaches. The framework also includes numerous performance enhancements and optimizations under the hood.

For example, Nuxt ensures that only necessary data is sent, and it intelligently prefetches data right before it's needed.

These optimizations, along with many others, contribute to the overall speed and efficiency of a Nuxt application. In essence, Nuxt provides a seamless user experience without sacrificing performance, offering developers the best of both SPA and SSR worlds.

If you want your link to open in a new tab (or window, depending on how the user’s browser works), you can use the target attribute:

<NuxtLink
    to="/articles"
  target="_blank"
>
    Mastering Nuxt 3
</NuxtLink>

22. Which Config to Use? runtimeConfig vs. appConfig

To better understand the differences and similarities between runtimeConfig and app.config, let's take a look at this feature comparison table (taken from the Nuxt documentation):

FeatureruntimeConfigapp.config
Client SideHydratedBundled
Environment Variables✅ Yes❌ No
Reactive✅ Yes✅ Yes
Types support✅ Partial✅ Yes
Configuration per Request❌ No✅ Yes
Hot Module Replacement❌ No✅ Yes
Non primitive JS types❌ No✅ Yes

Both runtimeConfig and app.config allow you to expose variables to your application. However, there are some key differences:

  1. runtimeConfig supports environment variables, whereas app.config does not. This makes runtimeConfig more suitable for values that need to be specified after the build using environment variables.
  2. runtimeConfig values are hydrated on the client side during run-time, while app.config values are bundled during the build process.
  3. app.config supports Hot Module Replacement (HMR), which means you can update the configuration without a full page reload during development.
  4. app.config values can be fully typed with TypeScript, whereas runtimeConfig cannot.

To decide whether to use runtimeConfig or app.config, I’d think about it this way:

  • runtimeConfig: Use runtimeConfig for private or public tokens that need to be specified after the build using environment variables. This is ideal for sensitive information or values that may change between different environments.
  • app.config: Use app.config for public tokens that are determined at build time, such as website configuration (theme variant, title) or any project config that are not sensitive. Since app.config supports HMR, it is particularly helpful for values that you may want to update during development without a full page reload.

23. Using runtimeConfig

The runtimeConfig is used to expose environment variables and private tokens within your application, such as API keys or other sensitive information. These values can be set in the nuxt.config.ts file and can be overridden using environment variables.

To set up private and public keys in your nuxt.config.ts file, you can use the following code example:

export default defineNuxtConfig({
  runtimeConfig: {
    // The private keys which are only available server-side
    shoeStoreApiSecret: 'my-secret-key',
    // Keys within public are also exposed client-side
    public: {
      shoeStoreApiBase: '/shoe-api'
    }
  }
})

To access runtimeConfig values within your application, you can use the useRuntimeConfig composable:

<script setup lang="ts">
const { shoeStoreApiBase } = useRuntimeConfig();
console.log(shoeStoreApiBase); // /shoe-api
</script>

Note that you can’t access a private key on the client-side:

<script setup lang="ts">
const { shoeStoreApiSecret } = useRuntimeConfig();
console.log(shoeStoreApiSecret); // undefined
</script>

But you can access all values in a server route:

export default defineEventHandler(async (event) => {
  const { shoreStoreApiSecret } = useRuntimeConfig();
  console.log(shoeStoreApiSecret); // my-secret-key
});

You can set environment variables in a .env file to make them accessible during development and build/generate. Just make sure that you use the right prefixes.

Put NUXT_ before everything, and don’t forget to add in PUBLIC if it’s a value in the public field of your config:

NUXT_PUBLIC_SHOE_STORE_API_BASE_URL = "https://api.shoestore.com"
NUXT_SHOE_STORE_API_SECRET = "my-secret-key"

24. Using appConfig

The app.config is used to expose public variables that can be determined at build time, such as theme variants, titles, or other non-sensitive project configurations. These values are set in the app.config.ts file.

To define app.config variables, you need to create the app.config.ts file in the root of your project:

// app.config.ts

export default defineAppConfig({
  theme: {
    primaryColor: '#ababab'
  }
})

To access app.config values within your application, you can use the useAppConfig composable:

<script setup lang="ts">
const appConfig = useAppConfig()
</script>

Although the appConfig type is automatically inferred, you can manually type your app.config using TypeScript if you really need to. This example is from the docs:

// index.d.ts
declare module 'nuxt/schema' {
  interface AppConfig {
    // This will entirely replace the existing inferred `theme` property
    theme: {
      // You might want to type this value to add more specific types than Nuxt can infer,
      // such as string literal types
      primaryColor?: 'red' | 'blue'
    }
  }
}

If you’re writing a module that needs config, you can also provide a type for your module.

Wrapping Up

If you enjoyed these tips, please do me a huge favour and share the article!

Nuxt is an incredible tool, and everyone should know these tips so they can use it to the fullest.

If you want to learn more about Nuxt, the best way to do that is through the Mastering Nuxt 3 course.

Michael Thiessen
Michael is a passionate full time Vue.js and Nuxt.js educator. His weekly newsletter is sent to over 11,000 Vue developers, and he has written over a hundred articles for his blog and VueSchool.

Follow MasteringNuxt on