Implementing Image Caching with Tauri: Enhancing Performance and Offline Access
Introduction
Delivering a seamless user experience is paramount. One key factor influencing user satisfaction is the speed at which images load within applications. By implementing image caching in Tauri applications, we can significantly improve performance, reduce latency, and provide offline access to users. In this article, we’ll explore how to effectively implement image caching in Tauri applications, harnessing the power of both web and native technologies.
The basic concept of image caching
Image caching involves storing images locally on a user’s device to quickly retrieve and display them when needed. Caching reduces load times, conserves network bandwidth, and ensures that users can still access content offline.
Before diving into image caching, ensure a basic Tauri application is set up. If you’re new to Tauri, refer to the official documentation for guidance on project creation and setup.
Getting started
Start by identifying the images in your application that could benefit from caching. These might include logos, icons, product images, or other static visuals.
We can store those specific images by using our custom component that caches images
1- add this line to the allow list in /src-tauri/tauri.config.json
"fs": { "all": true, "scope": ["$APPDATA", "$APPDATA/*", "$APPDATA/**"] },
this line will allow you to use the fs API with the folder $APPDATA where we will store cached images
2- You will need the following utils functions in /src/utils/image-caching.ts
import { fs } from "@tauri-apps/api";
import { BaseDirectory } from "@tauri-apps/api/fs";
import axios from "axios";
const CACHE_DIR = "cache";
/**
* Cache an image from a given URL to the specified cache directory.
*
* @param {string} imageUrl - The URL of the image to be cached.
* @returns {Promise<void>} A promise that resolves when the image is successfully cached.
* @throws {Error} If there's an error fetching, creating the cache directory, or writing the image data.
*/
const cacheImage = async (imageUrl: string) => {
try {
const imageData = await fetchImageData(imageUrl);
const imageName = getImageName(imageUrl);
const imagePath = getImagePath(imageName);
await createCacheDirectory();
await writeImageDataToCache(imagePath, imageData);
console.log("Image cached successfully:", imagePath);
} catch (error) {
console.error("Error caching image:", error);
}
};
/**
* Fetch image data from the provided URL.
*
* @param {string} imageUrl - The URL of the image to fetch.
* @returns {Promise<ArrayBuffer>} A promise that resolves to the fetched image data.
*/
const fetchImageData = async (imageUrl: string) => {
try {
const response = await axios.get(imageUrl, { responseType: "arraybuffer" });
return response.data;
} catch (error) {
throw new Error("Error fetching image data: " + error);
}
};
/**
* Extracts the image name from the provided URL.
*
* @param {string} imageUrl - The URL of the image.
* @returns {string} The extracted image name.
*/
const getImageName = (imageUrl: string) => {
return imageUrl.substring(imageUrl.lastIndexOf("/") + 1);
};
/**
* Generates the full path to the cache directory.
*
* @param {string} imageName - The name of the image.
* @returns {string} The full path to the cache directory.
*/
const getImagePath = (imageName: string) => {
return `${CACHE_DIR}/${imageName}`;
};
/**
* Creates the cache directory if it doesn't exist.
*
* @returns {Promise<void>} A promise that resolves when the cache directory is created.
*/
const createCacheDirectory = async () => {
try {
await fs.createDir(CACHE_DIR, {
recursive: true,
dir: BaseDirectory.AppData,
});
} catch (error) {
throw new Error("Error creating cache directory: " + error);
}
};
/**
* Writes image data to the cache directory.
*
* @param {string} imagePath - The path to the image file.
* @param {ArrayBuffer} imageData - The image data to write.
* @returns {Promise<void>} A promise that resolves when the image data is written to the cache.
*/
const writeImageDataToCache = async (
imagePath: string,
imageData: ArrayBuffer
) => {
try {
await fs.writeBinaryFile(imagePath, new Uint8Array(imageData), {
dir: BaseDirectory.AppData,
});
} catch (error) {
throw new Error("Error writing image data to cache: " + error);
}
};
/**
* Display a cached image or cache and display a new image.
*
* @param {string} imageUrl - The URL of the image to be displayed or cached.
* @returns {Promise<string>} A promise that resolves to a base64-encoded image data URI or the original image URL.
* @throws {Error} If there's an error reading or caching the image.
* @example
* const imageUrl = "https://example.com/image.jpg";
* const cachedImage = await displayCachedImage(imageUrl);
* console.log(cachedImage); // Outputs a base64-encoded image data URI or the original image URL.
*/
export const displayCachedImage = async (imageUrl: string) => {
const imageName = getImageName(imageUrl);
const imagePath = getImagePath(imageName);
const imageExists = await fs.exists(imagePath, {
dir: BaseDirectory.AppData,
});
if (imageExists) {
// Read the binary file
const u8Array = await fs.readBinaryFile(imagePath, {
dir: BaseDirectory.AppData,
});
console.info("Returned from cache");
// Convert to base64 to consume it in the image tag
const base64Image = _arrayBufferToBase64(u8Array);
return base64Image;
} else {
// Cache the image
cacheImage(imageUrl);
return imageUrl;
}
};
/**
* Converts a Uint8Array to a base64-encoded Data URI.
*
* @param {Uint8Array} uint8Array - The Uint8Array to convert to base64.
* @returns {string} A Data URI in the format "data:image/jpg;base64,<base64String>".
* @example
* const byteArray = new Uint8Array([255, 216, 255, 224, 0, 16, 74, 70, ...]);
* const dataUri = _arrayBufferToBase64(byteArray);
* console.log(dataUri); // Outputs a base64-encoded Data URI.
*/
function _arrayBufferToBase64(uint8Array: Uint8Array) {
// Assuming 'uint8Array' is your Uint8Array
const base64String = btoa(String.fromCharCode(...uint8Array));
return `data:image/jpg;base64,${base64String}`;
}
More details about the above code
- cacheImage: Caches an image from a given URL to a specified cache directory.
- fetchImageData: Returns the fetched image data as an `ArrayBuffer`.
- getImageName: Extracts the image name from the provided URL.
- getImagePath: Generates the full path to the cache directory for the image.
- createCacheDirectory: Creates the cache directory if it doesn’t exist using the `fs` module.
- writeImageDataToCache: Writes image data to the cache directory as a binary file.
- displayCachedImage: Checks if an image exists in the cache using fs.exists. If the image exists, read the image data from the cache using fs.readBinaryFile. Converts the image data to base64. If the image doesn’t exist, call `cacheImage` to cache the image and return the original URL.
- _arrayBufferToBase64: Converts a Uint8Array to a base64-encoded Data URI.
- These functions work together to cache, fetch, display, and convert images using the Tauri framework and related libraries.
React Image component
import { useEffect, useState } from "react";
import { displayCachedImage } from "../utils/cach-image";
interface ImageProps {
src: string;
className: string;
}
export default function ImageCache(props: ImageProps) {
const [image, setImage] = useState("/thumbnail.png");
useEffect(() => {
loadImage();
}, []);
const loadImage = async () => {
const loadedImage = await displayCachedImage(props.src);
setImage(loadedImage);
};
return (
<div>
<img src={image} className={props.className} />
</div>
);
}
Now you can use the above component in any place of your Tauri app and cache the images to the disk. I hope this article helped you 😊
Additional Resources:
- Tauri Official Documentation: https://tauri.app/v1/guides/
- Tauri Filesystem API: https://tauri.app/v1/api/js/fs