Get image dimensions from api before the acual images

What I am trying to accomplish
I have a landing page, which has a mansory grid, that displays the images for the different sub-pages which are handled, via the builder visual cms. As this is a mansory grid and its important that all images acutally have their original size, I need to be able to make an api call to the cdn to really quickly retrieve the dimensions of the images so that i can render a placeholder of the images, before the load in. I know that i could just hardcode some dimensions for the pictures however this would result in a layout shift, after the images have loaded, because its not an ordinary grid, as mentioned above. If look all over the docs, but i could find a way to get this informations before hand so that i wont need to wait for the pictures to actually load in before I can display them correclty.

Screenshots or video link
Screenshot how it would look like if the pictures are loaded, and i want to always have this layout beforehand, just with static colors so that everything is already the right size
https://imgur.com/a/oJb9Fmr

Code stack you are integrating Builder with
I am using NextJs and the Builder.io SDK, however i dont need to do it via the SDK i could also just do it via a vanilla, rest api call. I am also using the package, react-responsive-masonry however i dont think this really matters.

Hello @noahw,

Welcome to the builder.io forum post.

You can use Builder’s Image API to access and download optimized versions of images that you’ve uploaded.

yeah i know but, there i need to wait for the pictures to load first before i know their size, but I want to know them beforehand so that i can already preallocated the space needed for the picture, so that I have a placeholder for the image before it loads. Some images might take idk maybe half a second to load, to there will be a CLS because all the other static content is already loaded, so I want to avoid this by creatinh a placholder with the same dimensions as the image so that this will be displayed immediatly after the first contenful paint and afterwards if the images have loaded i can just substitute the placeholder for it.

Hello @noahw,

Unfortunately, it’s not currently possible to retrieve the original size of an image without making an API call to the image API and then using JavaScript code to determine the actual width and height.

If you need dimensions, one approach is to create a data model that includes the image, width, height, and any other relevant information. This feature is already a requested enhancement, and you can upvote it here: Image Original Size Feature Request.

Thanks,

@noahw my solution to this problem was to find each page’s image URLs during the page build and running them through the probe-image-size package. Then I save the data to a React Context that wraps the <BuilderComponent> etc. This means that the data is only available for published pages, but that’s where it really matters.

1 Like

Oh yeah that sounds like a good solution, would you be able to maybe share the code, so that I could look at it because I have been trying to do this for a day now, but i can’t get it to work. I would really apperciate it if you would be so kind to just post the overall code structure to maybe a pastebin or to github. thanks in advance!

The whole code structure is a bit complex because I do a whole bunch of other trickery this way during the build. I hope these few snippets can get you going (I’m omitting a few specifics, import statements, and type annotations etc.).

There are basically 4 files involved — the server-rendered page.tsx that calls the builder API and extractData; the client component that renders builder content inside the context provider; the context file (separated so it can be cleanly imported); and the extractData function itself.

// [[...path]]/page.tsx
builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!)

export default async function Page ({ params: { path } }) {
  const pageContent = await builder.get('page', {/* settings */})
  const extractedData = pageContent ? await extractData(pageContent) : {}
  /* ...more site-specific setup */

  return (  
    <RenderBuilderContent content={pageContent}
                          extractedData={extractedData}
                          model="page"
    />
  )
}
// RenderBuilderContent.ts
// in it's own file because the BuilderComponent needs 'use client'
'use client'

export function RenderBuilderContent ({ content, extractedData, model }) {
  /* ...more site-specific setup */
  
  return (
    <ExtractedDataContext.Provider value={extractedData}>
      <BuilderComponent
          content={content}
          model={model}
      />
    </ExtractedDataContext.Provider>
  )
}
// ExtractedDataContext.ts
import { createContext } from 'react'

export default createContext<Record<string, any>>({})
// extractData.ts

// get the value of a key from a JSON string
// this could theoretically also be achieved with a walker function, but since builder blocks have a well-known shape this is fine
const valueForKeyRegExp = (key: string) => new RegExp(`("${key}":\\s?")([^"]+)(")`, 'gm')

export default async function extractData (page) {
  const extractedData = {
    images: {},
    priorityAssets: [],
    /* other stuff we want to get */
  }

  async function getImageData ({ url }) {
    try {
      // pull in image data directly from the builder API endpoint
      const result = await require('probe-image-size')(url, { open_timeout: 30000, follow_max: 10 })
      // and store them using the URL as the key
      extractedData.images[url] = { ...result }
    } catch (e) {
      console.warn(e)
    }
  }

  const blocks = page?.data?.blocks
  for (const [i, block] of (blocks ?? []).entries()) {
    const text = JSON.stringify(block)
    for (const key of ['image', 'poster', /* ...any other keys used for images */]) {
      const matches = text.matchAll(valueForKeyRegExp(key))
      for (const match of matches) {
        const url = match[2]
        if (url) {
          await getImageData({ url })
          // get assets from the first 2 blocks and mark them for fetchpriority
          if (i <= 2) {
            extractedData.priorityAssets.push(url)
          }
        }
      }
    }
  }

  return extractedData
}

I’ve been thinking that I could also just append the extractedData to the API response itself (i.e. the content object) instead of using a Context, but haven’t gotten around to testing this.

Hope this helps!

Thank you very much, this resolved my Problem and also gave me the opportunity to tweak my own RenderComponent even further if I need it in the future! Thank you again for your help!

Glad I could help :​​)
Feel free to update here if you come up with additional use cases for this. I’m curious what else we can come up with.

Also, here’s an example of the implementation, including the sizes and priorityAssets (I’m hoping more people find this post useful):

// myCustomImageComponent.tsx
export default myCustomImageComponent ({ imageUrl, alt }) {
  const extractedData = useContext(ExtractedDataContext) ?? {}
  const isPriorityAsset = extractedData.priorityAssets?.includes(imageUrl)

  return (
    <img src={imageUrl} 
         alt={alt}
         width={extractedData[imageUrl]?.width}
         height={extractedData[imageUrl]?.height}
         loading={isPriorityAsset ? undefined : 'lazy'}
         fetchPriority={isPriorityAsset ? 'high' : undefined}
    />
  )
}

Note that optional chaining is required, since new components in a page will not have these properties yet, until the page is published in Builder and rebuilt (that tripped me up a bit at first).

1 Like