<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Underdog</title>
    <description>The latest articles on DEV Community by Underdog (@underdog_a894c070a2c6ece5).</description>
    <link>https://dev.to/underdog_a894c070a2c6ece5</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1641517%2F22fa4f6f-f395-4251-b293-f18d6e4ca996.png</url>
      <title>DEV Community: Underdog</title>
      <link>https://dev.to/underdog_a894c070a2c6ece5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/underdog_a894c070a2c6ece5"/>
    <language>en</language>
    <item>
      <title>From Base64 to S3 storage: Solving React-Quill Image Paste Performance Issues</title>
      <dc:creator>Underdog</dc:creator>
      <pubDate>Mon, 02 Mar 2026 15:58:49 +0000</pubDate>
      <link>https://dev.to/underdog_a894c070a2c6ece5/from-base64-to-s3-solving-react-quill-image-paste-performance-issues-1khm</link>
      <guid>https://dev.to/underdog_a894c070a2c6ece5/from-base64-to-s3-solving-react-quill-image-paste-performance-issues-1khm</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;br&gt;
When building a rich text editor for content management, one common challenge is handling images pasted from sources like Microsoft Word or other websites. These images often come as base64-encoded strings embedded directly in the content.&lt;/p&gt;

&lt;p&gt;The react-quill library, despite being a powerful rich text editor, doesn't handle this out of the box. By default, when users paste content with images (from Word, Google Docs, or web pages), react-quill preserves the base64-encoded images directly in the editor's content. &lt;/p&gt;

&lt;p&gt;This means: While this works for small images, it creates significant performance issues:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow page loads&lt;/strong&gt;: Articles with multiple base64 images can take 10+ seconds to load&lt;/p&gt;

&lt;p&gt;Our editor was suffering from exactly this issue. Articles with embedded images were taking up to 10 seconds to load. After implementing our solution, load times dropped to under 0.5 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;&lt;br&gt;
We created a robust image handling parser that automatically intercepts pasted images, uploads them to S3 storage, and replaces the base64 data with cloud URLs. Here's how we achieved it.&lt;/p&gt;

&lt;p&gt;Key Components&lt;br&gt;
&lt;strong&gt;1. Registering Custom Matchers&lt;/strong&gt;&lt;br&gt;
The crucial first step is registering matchers in the Quill clipboard module. This tells Quill to intercept any pasted content containing images and process it through our custom handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const modules = useMemo(
  () =&amp;gt; ({
    toolbar: {...},
    imageDropAndPaste: {...},
    clipboard: {
      matchers: [
        ['IMG', imgMatcher],      // Process img tags
        ['PICTURE', imgMatcher],  // Process picture tags
      ],
    },
  }),
  [],
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and pass it to props in React-quill component like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;ReactQuill
      modules={modules}
      data-aid={testId}
      ref={quillRef}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before uploading to S3, we need util function which convert base64 strings to File objects. But You can use any of your own utilities or third-party libraries for base64 to File conversion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getContentTypeFromBase64 = (dataUrl: string) =&amp;gt; {
  if (!dataUrl.startsWith('data:')) return null

  const match = dataUrl.match(/^data:(.*?);base64,/)
  return match ? match[1] : null
}


export const base64toFile = ({
  base64,
  name,
}: {
  base64: string
  name?: string
}): File =&amp;gt; {
  // Extract content type from base64 header
  const contentType = base64.match(/^data:(.*?);base64,/)?.[1] || 'image/png'

  // Decode base64 to binary data
  const byteString = atob(base64.split(',')[1] || base64)
  const byteArray = new Uint8Array(byteString.length)

  for (let i = 0; i &amp;lt; byteString.length; i++) {
    byteArray[i] = byteString.charCodeAt(i)
  }

  return new File([byteArray], name ?? uuidv4(), { type: contentType })
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then look up at our function that parse it from base64 to s3, get new urls from s3 storage and implement it into our editor&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Delta from 'quill-delta'
import { base64toFile } from '/path/to/util'
import ReactQuill from 'react-quill-new'

export const imageMatcherUtil = (
  handleImageUpload: (file: File) =&amp;gt; Promise&amp;lt;string | undefined&amp;gt;,
  quillRef: React.RefObject&amp;lt;ReactQuill | null&amp;gt;,
) =&amp;gt; {
  return (_: void, delta: Delta) =&amp;gt; {
    // Step 1: Filter for base64 images in the pasted content
    const base64Images = delta.ops.filter(
      (op) =&amp;gt;
        typeof op.insert === 'object' &amp;amp;&amp;amp;
        typeof op.insert.image === 'string' &amp;amp;&amp;amp;
        'image' in op.insert &amp;amp;&amp;amp;
        op.insert?.image?.startsWith('data:'),
    )

    // Step 2: If no base64 images or no upload handler, return original delta
    if (base64Images.length === 0 || !handleImageUpload) {
      return delta
    }

    // Step 3: Process each base64 image asynchronously
    base64Images.forEach((op) =&amp;gt; {
      const insert = op.insert as Record&amp;lt;string, string&amp;gt;
      const base64File = insert.image

      // Convert base64 string to File object
      const parsedFile = base64toFile({ base64: base64File })

      // Upload to cloud storage
      handleImageUpload(parsedFile).then((url) =&amp;gt; {
        if (url &amp;amp;&amp;amp; quillRef.current) {
          const quill = quillRef.current.getEditor()
          const contents = quill.getContents()
          let position = 0

          // Step 4: Find and replace each base64 image with cloud URL
          for (const contentOp of contents.ops) {
            if (
              typeof contentOp.insert === 'object' &amp;amp;&amp;amp;
              contentOp.insert?.image === base64File
            ) {
              // Replace base64 with cloud URL in the document
              quill.updateContents(
                new Delta().retain(position).delete(1).insert({ image: url }),
              )
              break
            }
            // Track position for accurate replacement
            position +=
              typeof contentOp.insert === 'string' ? contentOp.insert.length : 1
          }
        }
      })
    })

    // Return original delta - the async replacement happens separately
    return delta
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it !&lt;/p&gt;

</description>
      <category>reactquill</category>
      <category>editor</category>
      <category>frontend</category>
      <category>react</category>
    </item>
  </channel>
</rss>
