Async custom image component

Based on the custom component template, I’ve made a clickable image component. It works as expected; when you click on an image, the image has a boolean state that is inverted. However, I have some questions to ask in regards to optimisation:

  1. Is it possible to load images in parallel instead of creating each one sequentially? What is the fastest way to load ~500 images on a page? I am using st.exerimental_memo() for caching requests I make for each image, but the images still take roughly 30 seconds per 100 images.

  2. How does streamlit handle local image URLs/URIs? I can pass a local file path to st.image and it loads fine, but when passing the path to my custom component, it refuses to load due to javascript not being able to access local files. My workaround, as you can see below, is serving such images on a local server as base64 files.

import os
import requests
import streamlit.components.v1 as components

_RELEASE = False

if not _RELEASE:
    _component_func = components.declare_component("interactive_image", url="http://localhost:3001")
else:
    parent_dir = os.path.dirname(os.path.abspath(__file__))
    build_dir = os.path.join(parent_dir, "frontend/build")
    _component_func = components.declare_component("interactive_image", path=build_dir)

def interactive_image(path, labelling=False, format="JPEG", key=None):
    component_value = _component_func(path=path, labelling=labelling, format=format, key=key, default=0)
    return component_value

if not _RELEASE:
    import streamlit as st
    from itertools import cycle

    st.set_page_config(layout="wide")
    with st.sidebar:
        st.write("Example sidebar")

    st.subheader("Interactive image component")
    path = f"http://{HOST}:{PORT}/image.png"

    # Decode base64 into ascii and convert with "const new_path = "data:image/{format};base64," + path"
    labelling = False
    cols = cycle(st.columns(4))

    for idx in range(4):
        with next(cols):
            labelling = not labelling
            st.write(labelling)
            r = requests.get(path)
            data_uri = r.content.decode('ascii')
            interactive_image(data_uri, labelling, "PNG", key=idx)
import {
  Streamlit,
  StreamlitComponentBase,
  withStreamlitConnection,
} from "streamlit-component-lib"
import React, { ReactNode } from "react"

interface State {
  selected: boolean
}

class InteractiveImage extends StreamlitComponentBase<State> {
  public state = { selected: false }
  readonly labelling = this.props.args["labelling"]

  public render = (): ReactNode => {
    // Variables
    const format = this.props.args["format"]
    const { theme } = this.props
    const style: React.CSSProperties = {}

    // Determing correct path
    let path
    if (format === "PNG" || format === "png") {
      path = "data:image/png;base64," + this.props.args["path"]
    } else {
      path = "data:image/jpeg;base64," + this.props.args["path"]
    }

    // Styling
    if (theme) {
      let borderStyling = `3px solid ${
        this.state.selected ? theme.primaryColor : theme.backgroundColor
      }`
      if (this.labelling) {
        borderStyling = `3px solid ${theme.primaryColor}`
        style.filter = "grayscale(100%)"
        style.WebkitFilter = "grayscale(100%)" /* Safari 6.0 - 9.0 */
      }
      style.border = borderStyling
      style.outline = borderStyling
      style.borderRadius = "4px"
    }

    // Image
    return (
      <img
        style={style}
        width={this.props.width}
        src={path}
        srcSet={path}
        alt={path}
        loading="lazy"
        onLoad={this.onLoad}
        onClick={this.onClicked}
        key={path}
        data-testid="stInteractiveImage"
      />
    )
  }

  // Invert current state of image when clicked
  private onClicked = (): void => {
    if (this.labelling !== true) {
      this.setState(
        (prevState) => ({ selected: !prevState.selected }),
        () => Streamlit.setComponentValue(this.state.selected)
      )
    }
  }

  // When the image has finished loading, update it's height for correct rendering
  private onLoad = (): void => {
    Streamlit.setFrameHeight()
  }
}

export default withStreamlitConnection(InteractiveImage)