Prismic, Next js, performance optimization -

Working with Next JS, Prismic and Postgre SQL for external database, I am having bad performances on my key pages which are all making call to Prismic API to display some content.
Basically having a crazy initial server response (4s to 9s) and Total Blocking Time (1,400ms+)

Could it be because of how I have set up the fetching with Prismic's client?

Here's the code snippet for the homepage:

import { Company, Job } from '@prisma/client'
import JobAlertsCtaBasic from '@/components/JobAlertsCtaBasic'

interface Props {
  metaTitle: string
  metaDescription: string
  ogImage: string
  page: any
  featuredJobs: (Job & { company: Company })[]
}

const Homepage: FC<Props> = ({ metaTitle, metaDescription, ogImage, page }) => {
  const router = useRouter()
  const canonicalUrl = `https://dataaxy.com` + router.asPath
  const ogUrl = canonicalUrl

  function structuredDataOrg() {
export async function getServerSideProps({ previewData }) {
  try {
    const clientPrismic = createClient({ previewData })
    const pagePrismic = await clientPrismic.getSingle('homepage')

    return {
      props: {
        metaTitle: pagePrismic.data.meta_title,
        metaDescription: pagePrismic.data.meta_description,
        ogImage: pagePrismic.data.og_image.url,
        page: pagePrismic
      }
    }
  } catch (err: any) {
    console.error(err)
    return { props: { error: err.message } }
  }
}

but getStaticProps for the [uid].tsx:

xport async function getStaticProps({ params, previewData }) {
  const client = createClient({ previewData })

  const page = await client.getByUID('pillar_page', params.uid)
  const similarPages = await getSimilarPages({ client, page })

  const slices = await Promise.all(
    page.data.slices.map(async (slice) => {
      if (
        slice.slice_type === 'reusable_topic_info' &&
        slice.primary.reusabletopic.id
      ) {
        const topic = await client.getByID(slice.primary.reusabletopic.id)
        slice.primary.reusabletopic = topic
        return slice
      }

      return slice
    })
  )

  return {
    props: {
      metaTitle: page.data.meta_title,
      metaDescription: page.data.meta_description,
      ogImage: page.data.og_image.url,
      slices,
      similarPages
    }
  }
}

export async function getStaticPaths() {
  const client = createClient()

  const pages = await client.getAllByType('pillar_page')

  return {
    paths: pages.map((page) => prismicH.asLink(page)),
    fallback: false
  }
}

am I doing something wrongly? Is Prismic not optimized for displaying data coming from an external database? (here for instance, jobs which are coming from an external DB)

Here's the website if you want to test pageSpeed: https://dataaxy.com/jobs/data-visualization-jobs

PS: I am using next js 13.2 with the old /pages set up.

Thanks for your help!

Hey there, @Marving , sorry to hear you're having trouble. Those kinds of response times are definitely not typical for Prismic, I think the issue probably comes down to chaining requests and where all of these calls are happening.

I don't really understand what you're showing me for the first snippet, it seems like some important information got cut off, like your render function. Could you share the whole page?

For the homepage's getServerSideProps it's good that you're fetching the Prismic content within it, but because you're not also making the additional API call within getServerSideProps that call is going to happen client-side, which explains those long wait times.

You need to make all of your API calls within either getStaticProps or getServerSideProps for your site to benefit from Next.js's rendering features. Any queries that happen within Slices or any other components will happen on the Client and need to wait for all your JS to load, be processed, and execute, then the request sends, the request comes back, and the request runs.

Can you show me the component that is making that additional query so I can confirm?

Instead of getServerSideProps for the homepage, have you considered ISR? That might give you performance benefits while still keeping your site relatively up to date.

Hello @alex.trost ,

Thank you for coming back to me!
Basically, the component is a advanced Filter system, visibile here: Entry level data analyst - Dataaxy
So depending on the filters selected by the users, different jobs will be returned.
That is why we are currently doing the following:

A JobsListSlice slice, returns a Next.js component, JobsList.
JobsList calls a custom hook, useJobsQuery.
useJobsQuery makes an API call to /api/jobs to fetch the jobs data.

Are you saying that I should not call useJobsQuery in the component JobsList, but directly doing it at the page template level [uid].tsx, in it's getStaticProps?

Here are the different part of the codes:
JobsListSlice

import JobsList from '@/components/JobsList'
/**
 * @typedef {import("@prismicio/client").Content.HowWeWorkSlice} HowWeWorkSlice
 * @typedef {import("@prismicio/react").SliceComponentProps<HowWeWorkSlice>} HowWeWorkProps
 * @param { HowWeWorkProps }
 */
const JobsListSlice = ({ slice }) => <JobsList slice={slice} />

export default JobsListSlice

JobsList.tsx:

import { useState } from 'react'
import { useRouter } from 'next/router'
import { useForm } from 'react-hook-form'
import useCollection from '@/hooks/useCollection'
import useJobsQuery from '@/hooks/useJobsQuery'
import BaseDataCollection from '@/components/common/BaseDataCollection'
import JobCard from '@/components/JobCard'
import Filters from '@/components/JobsFilters'

export default function JobsList({ slice }) {
  const router = useRouter()

  // this will be used to populate the filters and also refine the search
  const defaultValues = {
    search: slice.primary.prefiltered_terms,
    location: slice.primary.prefiltered_terms_location,
    keywords: slice.primary.prefiltered_terms
      ? [slice.primary.prefiltered_terms]
      : []
  }

  const form = useForm({
    defaultValues: {
      search: defaultValues.search,
      location: defaultValues.location
    }
  })

  const [formExtra, setFormExtra] = useState({
    keywords: defaultValues.keywords.map((value) => ({ value, label: value })),
    remoteType: [],
    seniorityLevels: [],
    workType: []
  })

  const onBuildParams = ({ values }) => {
    const { search } = values || null
    const newParams = {
      where: { status: 'active' },
      page: Number(router.query.page ?? 1)
    } as any

    if (search && search.length > 0) {
      newParams.where.title = { contains: search, mode: 'insensitive' }
      // newParams.where.description = { contains: search, mode: 'insensitive' }
    }

    if (values.location) {
      newParams.where.location = {
        contains: values.location,
        mode: 'insensitive'
      }
    }

    if (values.seniorityLevels && values.seniorityLevels.length > 0) {
      newParams.where.seniorityLevels = { hasSome: values.seniorityLevels }
    }

    if (values.remoteType && values.remoteType.length > 0) {
      newParams.where.remoteType = { in: values.remoteType }
    }

    newParams.where.workType = { in: values.workType }

    // if (values.keywords && values.keywords.length > 0) {
    //   newParams.where.keywords = {
    //     hasSome: values.keywords.map((keyword) => keyword.value)
    //   }
    // }

    return newParams
  }

  const {
    data: collection,
    handleClearFilters,
    handlePagination,
    handleSetFilters,
    isFetching,
  } = useCollection({
    defaultValues,
    onBuildParams,
    useQuery: useJobsQuery
  })

  return (
    <div className="container-constrained container-spacing mt-10">
      <div className="grid grid-cols-1 md:grid-cols-3 md:gap-6">
        <div>
          <Filters
            defaultValues={defaultValues}
            form={form}
            formExtra={formExtra}
            onChange={handleSetFilters}
            setFormExtra={setFormExtra}
          />
        </div>
        <div className="col-span-2">
          <BaseDataCollection
            Card={JobCard}
            collection={collection}
            handleClearFilters={handleClearFilters}
            handlePagination={handlePagination}
            isFetching={isFetching}
          />
        </div>
      </div>
    </div>
  )
}

useJobsQuery:

import { useQuery } from 'react-query'
import { useRouter } from 'next/router'
import apiService from '@/services/apiService'

export default function useJobsQuery({ params }) {
  const router = useRouter()

  return useQuery(
    ['jobs', params],
    async () => {
      return apiService.get('/api/jobs', {
        params: {
          ...params,
          page: router.query.page,
          perPage: 12,
          orderBy: { publicationDate: 'desc' },
          include: { company: true }
        }
      })
    },
    {
      enabled: router.isReady
    }
  )
}

The page template [uid].tsx:

import Head from 'next/head'
import { SliceZone } from '@prismicio/react'
import * as prismicH from '@prismicio/helpers'
import { createClient } from '@/prismicio'
import { components } from '@/slices'
import { useRouter } from 'next/router'
import getSimilarPages from '@/utils/getSimilarPages'
import Statistics from '@/components/Statistics'
import JobAlertsCtaBasic from '@/components/JobAlertsCtaBasic'

export default function Page({
  metaTitle,
  metaDescription,
  ogImage,
  slices,
  similarPages
}) {
  const router = useRouter()
  const canonicalUrl = `https://dataaxy.com` + router.asPath
  const ogUrl = canonicalUrl

 const metaTitleCompleted = `${metaTitle} - Dataaxy`

  return (
    <>
      <Head>
      ....
      </Head>
      <SliceZone
        slices={slices}
        components={components}
        context={{ similarPages }}
      />
      <Statistics />
      <JobAlertsCtaBasic />
    </>
  )
}

export async function getStaticProps({ params, previewData }) {
  const client = createClient({ previewData })

  const page = await client.getByUID('pillar_page', params.uid)
  const similarPages = await getSimilarPages({ client, page })

  const slices = await Promise.all(
    page.data.slices.map(async (slice) => {
      if (
        slice.slice_type === 'reusable_topic_info' &&
        slice.primary.reusabletopic.id
      ) {
        const topic = await client.getByID(slice.primary.reusabletopic.id)
        slice.primary.reusabletopic = topic
        return slice
      }

      return slice
    })
  )

  return {
    props: {
      metaTitle: page.data.meta_title,
      metaDescription: page.data.meta_description,
      ogImage: page.data.og_image.url,
      slices,
      similarPages
    }
  }
}

export async function getStaticPaths() {
  const client = createClient()

  const pages = await client.getAllByType('pillar_page')

  return {
    paths: pages.map((page) => prismicH.asLink(page)),
    fallback: false
  }
}

Thanks for your help!

Hey Marving! Thanks for sharing that code. Before checking it out I spoke with my colleague @angeloashmore and he let me know that you're currently checking to see if Sentry is the cause of the performance issues.

If that’s not it, could you give us a minimal reproducible example so that we can try it on our end?
From the code you posted and sent to Angelo you don’t seem to be doing anything wrong with your Prismic setup.

Let me know how it goes!

1 Like

Hello @alex.trost, thank you for coming back to me.
Indeed, I will try removing sentry, but either way I will be forced to use sentry in production.
Thanks to @angeloashmore we have found a few way to optimize the fetching set-up of Prismic slices and the results are already better.

Due to time constraints, I have to close the performance chapter for now. Thank you for your help!!