New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to preload / prefetch images for later use in ImageView? #2314

Closed
ms88privat opened this Issue Aug 12, 2015 · 19 comments

Comments

Projects
None yet
@ms88privat

ms88privat commented Aug 12, 2015

A possible solution is in this comment: #2233 (comment)

But i don't have any Obj-C skills yet. So maybe someone could help me out with this?

Thank you guys!

Marcel

@ide

This comment has been minimized.

Show comment
Hide comment
@ide

ide Aug 12, 2015

Collaborator

You could render an Image component with opacity 0 or size 0x0. Not a great solution but should work.

Collaborator

ide commented Aug 12, 2015

You could render an Image component with opacity 0 or size 0x0. Not a great solution but should work.

@ms88privat

This comment has been minimized.

Show comment
Hide comment
@ms88privat

ms88privat Aug 12, 2015

I thought about that, but it is not really suitable in general and especially in my situation :(.
But do you think it would be possible to write a native module for it and export it to Js? I would really like to help and contribute, but i am not able to do it by myself yet. Maybe i can put 30$ in the pot instead.

ms88privat commented Aug 12, 2015

I thought about that, but it is not really suitable in general and especially in my situation :(.
But do you think it would be possible to write a native module for it and export it to Js? I would really like to help and contribute, but i am not able to do it by myself yet. Maybe i can put 30$ in the pot instead.

@ide

This comment has been minimized.

Show comment
Hide comment
@ide

ide Aug 12, 2015

Collaborator

The hard part is figuring out the API. For example, do we want to expose some kind of network cache that is shared by XHR/fetch and other networking APIs? I think the implementation will be pretty straightforward.

Collaborator

ide commented Aug 12, 2015

The hard part is figuring out the API. For example, do we want to expose some kind of network cache that is shared by XHR/fetch and other networking APIs? I think the implementation will be pretty straightforward.

@hayeah

This comment has been minimized.

Show comment
Hide comment
@hayeah

hayeah Aug 14, 2015

Contributor

@ms88privat Here's the prefetching NativeModule I came up with. It uses the same HTTP cache that the original Image component uses for remote images.

(BTW, to get it to work, you'll need to add node_modules/Libraries to the project's search path, and set it to "recursive")

ImagePreloadManager.prefetch(url,function(err,result) {
  // if(err) ...
  var {width,height,bytes} = result;
  // ...
});

RCTImagePreloadManager.h

#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"


@interface RCTImagePreloadManager : NSObject <RCTBridgeModule>

@end

RCTImagePreloadManager.m

#import "RCTImagePreloadManager.h"
#import "RCTImageDownloader.h"
#import "RCTUtils.h"

@implementation RCTImagePreloadManager

RCT_EXPORT_MODULE(); // actually exported as "ImagePreloadManager"


//
RCT_EXPORT_METHOD(prefetch:(NSString *) src
                onComplete:(RCTResponseSenderBlock)completeBlock) {
  NSURL *url =  [NSURL URLWithString:src];
  [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:^(int64_t written, int64_t total) {
    NSLog(@"(%.2f) %@\n",((float) written)/total,url);
  } block:^(NSData *data, NSError *error) {
    if(error != nil) {
//      NSLog(@"image preload fail: %@\n (%@)",url,error);
      completeBlock(@[error.localizedDescription]);
    } else {
//      CGFloat scale = RCTScreenScale();
      UIImage *image = [UIImage imageWithData:data scale:1];
//      NSLog(@"image preload success: (%.2fx%.2f) %@\n",image.size.width,image.size.height, url);
      completeBlock(@[[NSNull null],@{
                        @"width": @(image.size.width),
                        @"height": @(image.size.height),
                        @"bytes": @(data.length),
                        }]);
    }
  }];
}

@end
Contributor

hayeah commented Aug 14, 2015

@ms88privat Here's the prefetching NativeModule I came up with. It uses the same HTTP cache that the original Image component uses for remote images.

(BTW, to get it to work, you'll need to add node_modules/Libraries to the project's search path, and set it to "recursive")

ImagePreloadManager.prefetch(url,function(err,result) {
  // if(err) ...
  var {width,height,bytes} = result;
  // ...
});

RCTImagePreloadManager.h

#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"


@interface RCTImagePreloadManager : NSObject <RCTBridgeModule>

@end

RCTImagePreloadManager.m

#import "RCTImagePreloadManager.h"
#import "RCTImageDownloader.h"
#import "RCTUtils.h"

@implementation RCTImagePreloadManager

RCT_EXPORT_MODULE(); // actually exported as "ImagePreloadManager"


//
RCT_EXPORT_METHOD(prefetch:(NSString *) src
                onComplete:(RCTResponseSenderBlock)completeBlock) {
  NSURL *url =  [NSURL URLWithString:src];
  [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:^(int64_t written, int64_t total) {
    NSLog(@"(%.2f) %@\n",((float) written)/total,url);
  } block:^(NSData *data, NSError *error) {
    if(error != nil) {
//      NSLog(@"image preload fail: %@\n (%@)",url,error);
      completeBlock(@[error.localizedDescription]);
    } else {
//      CGFloat scale = RCTScreenScale();
      UIImage *image = [UIImage imageWithData:data scale:1];
//      NSLog(@"image preload success: (%.2fx%.2f) %@\n",image.size.width,image.size.height, url);
      completeBlock(@[[NSNull null],@{
                        @"width": @(image.size.width),
                        @"height": @(image.size.height),
                        @"bytes": @(data.length),
                        }]);
    }
  }];
}

@end
@ms88privat

This comment has been minimized.

Show comment
Hide comment
@ms88privat

ms88privat Aug 16, 2015

@hayeah thank you very much. I will try it tomorrow 👍

ms88privat commented Aug 16, 2015

@hayeah thank you very much. I will try it tomorrow 👍

@ms88privat

This comment has been minimized.

Show comment
Hide comment
@ms88privat

ms88privat Aug 17, 2015

@hayeah What I am doing wrong? (using 0.10.0-rc)

  1. I copied the files into a folder which i then added to my project through xcode.
    https://db.tt/GenldugP
  2. I set the Search-Path: (*it is node_modules/react-native/Libraries instead of node_modules/Libraries?)
    https://db.tt/eE5d25eu

But then i get these errors.
https://db.tt/WNXFyAnk

Thank you

ms88privat commented Aug 17, 2015

@hayeah What I am doing wrong? (using 0.10.0-rc)

  1. I copied the files into a folder which i then added to my project through xcode.
    https://db.tt/GenldugP
  2. I set the Search-Path: (*it is node_modules/react-native/Libraries instead of node_modules/Libraries?)
    https://db.tt/eE5d25eu

But then i get these errors.
https://db.tt/WNXFyAnk

Thank you

@hayeah

This comment has been minimized.

Show comment
Hide comment
@hayeah

hayeah Aug 17, 2015

Contributor

@ms88privat Xcode is complaining that method is missing. Maybe 0.10.0rc removed the method I used? Check the header to see if that method exists.

(It looks like you've done 1 and 2 properly)

Contributor

hayeah commented Aug 17, 2015

@ms88privat Xcode is complaining that method is missing. Maybe 0.10.0rc removed the method I used? Check the header to see if that method exists.

(It looks like you've done 1 and 2 properly)

@ms88privat

This comment has been minimized.

Show comment
Hide comment
@ms88privat

ms88privat Aug 20, 2015

@hayeah I am lost here, did you try it with 0.10.0-rc?

ms88privat commented Aug 20, 2015

@hayeah I am lost here, did you try it with 0.10.0-rc?

@brentvatne

This comment has been minimized.

Show comment
Hide comment
@brentvatne

brentvatne Sep 14, 2015

Collaborator

The signature for downloadDataForURL is now:

- (RCTImageLoaderCancellationBlock)downloadDataForURL:(NSURL *)url
                                      progressHandler:(RCTImageLoaderProgressBlock)progressBlock
                                    completionHandler:(void (^)(NSError *error, NSData *data))completionBlock

So:

RCT_EXPORT_METHOD(prefetch:(NSString *) src
                onComplete:(RCTResponseSenderBlock)completeBlock) {
  NSURL *url =  [NSURL URLWithString:src];
  [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressHandler:^(int64_t written, int64_t total) {
    NSLog(@"(%.2f) %@\n",((float) written)/total,url);
  } completionHandler:^(NSData *data, NSError *error) {
    if(error != nil) {
//      NSLog(@"image preload fail: %@\n (%@)",url,error);
      completeBlock(@[error.localizedDescription]);
    } else {
//      CGFloat scale = RCTScreenScale();
      UIImage *image = [UIImage imageWithData:data scale:1];
//      NSLog(@"image preload success: (%.2fx%.2f) %@\n",image.size.width,image.size.height, url);
      completeBlock(@[[NSNull null],@{
                        @"width": @(image.size.width),
                        @"height": @(image.size.height),
                        @"bytes": @(data.length),
                        }]);
    }
  }];
}
Collaborator

brentvatne commented Sep 14, 2015

The signature for downloadDataForURL is now:

- (RCTImageLoaderCancellationBlock)downloadDataForURL:(NSURL *)url
                                      progressHandler:(RCTImageLoaderProgressBlock)progressBlock
                                    completionHandler:(void (^)(NSError *error, NSData *data))completionBlock

So:

RCT_EXPORT_METHOD(prefetch:(NSString *) src
                onComplete:(RCTResponseSenderBlock)completeBlock) {
  NSURL *url =  [NSURL URLWithString:src];
  [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressHandler:^(int64_t written, int64_t total) {
    NSLog(@"(%.2f) %@\n",((float) written)/total,url);
  } completionHandler:^(NSData *data, NSError *error) {
    if(error != nil) {
//      NSLog(@"image preload fail: %@\n (%@)",url,error);
      completeBlock(@[error.localizedDescription]);
    } else {
//      CGFloat scale = RCTScreenScale();
      UIImage *image = [UIImage imageWithData:data scale:1];
//      NSLog(@"image preload success: (%.2fx%.2f) %@\n",image.size.width,image.size.height, url);
      completeBlock(@[[NSNull null],@{
                        @"width": @(image.size.width),
                        @"height": @(image.size.height),
                        @"bytes": @(data.length),
                        }]);
    }
  }];
}

@brentvatne brentvatne closed this Sep 14, 2015

@andyyou

This comment has been minimized.

Show comment
Hide comment
@andyyou

andyyou Oct 22, 2015

BTW is any possible do this mission as follow

fetch(imageURL)
.then(function(response) {
  return response.blob();
})
.then(function(response) {
  /* get image blob or base64 format*/
})
.done();

andyyou commented Oct 22, 2015

BTW is any possible do this mission as follow

fetch(imageURL)
.then(function(response) {
  return response.blob();
})
.then(function(response) {
  /* get image blob or base64 format*/
})
.done();
@faurehu

This comment has been minimized.

Show comment
Hide comment
@faurehu

faurehu Mar 2, 2016

@andyyou did this work for you? My response comes with an empty body

faurehu commented Mar 2, 2016

@andyyou did this work for you? My response comes with an empty body

@ms88privat

This comment has been minimized.

Show comment
Hide comment
@ms88privat

ms88privat Mar 2, 2016

@faurehu Andyyou's solution is not implemented I think, it was only a suggestion. You can use the solution from above (tested on RN 0.16.0):

Add these two files to your iOS project:

RCTImagePreloadManager.h

#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"


@interface RCTImagePreloadManager : NSObject <RCTBridgeModule>

@end

RCTImagePreloadManager.m

#import "RCTImagePreloadManager.h"
#import "RCTImageView.h"
#import "RCTBridge.h"
#import "RCTImageLoader.h"

@implementation RCTImagePreloadManager

RCT_EXPORT_MODULE(); // actually exported as "RCTImagePreloadManager"

@synthesize bridge = _bridge;

//
RCT_EXPORT_METHOD(prefetch:(NSString *) src
                  onComplete:(RCTResponseSenderBlock)completeBlock) {
  [self.bridge.imageLoader loadImageWithTag:src size:CGSizeMake(1, 1) scale:1 resizeMode:UIViewContentModeScaleAspectFill progressBlock:^(int64_t progress, int64_t total) {} completionBlock:^(NSError *error, UIImage *image) {
    if(error != nil) {
      completeBlock(@[error.localizedDescription]);
    } else {
      completeBlock(@[[NSNull null],@{
                        @"width": @(image.size.width),
                        @"height": @(image.size.height)
                        }]);
    }
  }];
}
@end

And use it like so in javascript:

import { NativeModules } from 'react-native';
const ImagePreloadManager = NativeModules.ImagePreloadManager;
ImagePreloadManager.prefetch(url, function(err,result) {
                // if(err) ...
                // var {width,height,bytes} = result;
            });

Thats it.

@nicklockwood But I think there is another solution integrated in react-native by now. But I can't remember it and I did not found it in the documentation?

ms88privat commented Mar 2, 2016

@faurehu Andyyou's solution is not implemented I think, it was only a suggestion. You can use the solution from above (tested on RN 0.16.0):

Add these two files to your iOS project:

RCTImagePreloadManager.h

#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"


@interface RCTImagePreloadManager : NSObject <RCTBridgeModule>

@end

RCTImagePreloadManager.m

#import "RCTImagePreloadManager.h"
#import "RCTImageView.h"
#import "RCTBridge.h"
#import "RCTImageLoader.h"

@implementation RCTImagePreloadManager

RCT_EXPORT_MODULE(); // actually exported as "RCTImagePreloadManager"

@synthesize bridge = _bridge;

//
RCT_EXPORT_METHOD(prefetch:(NSString *) src
                  onComplete:(RCTResponseSenderBlock)completeBlock) {
  [self.bridge.imageLoader loadImageWithTag:src size:CGSizeMake(1, 1) scale:1 resizeMode:UIViewContentModeScaleAspectFill progressBlock:^(int64_t progress, int64_t total) {} completionBlock:^(NSError *error, UIImage *image) {
    if(error != nil) {
      completeBlock(@[error.localizedDescription]);
    } else {
      completeBlock(@[[NSNull null],@{
                        @"width": @(image.size.width),
                        @"height": @(image.size.height)
                        }]);
    }
  }];
}
@end

And use it like so in javascript:

import { NativeModules } from 'react-native';
const ImagePreloadManager = NativeModules.ImagePreloadManager;
ImagePreloadManager.prefetch(url, function(err,result) {
                // if(err) ...
                // var {width,height,bytes} = result;
            });

Thats it.

@nicklockwood But I think there is another solution integrated in react-native by now. But I can't remember it and I did not found it in the documentation?

@nicklockwood

This comment has been minimized.

Show comment
Hide comment
@nicklockwood

nicklockwood Mar 2, 2016

Contributor

You can use Image.getSize(url) on iOS, which will do a partial preload. We don't have an official preloading solution yet.

Contributor

nicklockwood commented Mar 2, 2016

You can use Image.getSize(url) on iOS, which will do a partial preload. We don't have an official preloading solution yet.

@dcworldwide

This comment has been minimized.

Show comment
Hide comment
@dcworldwide

dcworldwide Jun 13, 2016

I'd imagine this is a common optimisation most apps make. After testing my photography app on a medium BW network it's clear to me I need to implement image pre-fetching.

dcworldwide commented Jun 13, 2016

I'd imagine this is a common optimisation most apps make. After testing my photography app on a medium BW network it's clear to me I need to implement image pre-fetching.

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Sep 5, 2016

The Image component in React comes with a static method for preloading called prefetch(url) although based on the docs it's unclear how it's actually used. It's combined with 10+ examples and is a mess to follow.

ghost commented Sep 5, 2016

The Image component in React comes with a static method for preloading called prefetch(url) although based on the docs it's unclear how it's actually used. It's combined with 10+ examples and is a mess to follow.

@ronnyhartenstein

This comment has been minimized.

Show comment
Hide comment
@ronnyhartenstein

ronnyhartenstein Sep 29, 2016

@ChristianTucker Yes, the offical examples here are a big ball of mud. But still it helps me.

In my scenario I want to prefetch a lot of really small images (~1000 of 1kB) and want to do it on start. So if the prefetch of an image URI is still finished, then <Image ..> should use them from store. Works well, but it prefetches nearly every time. If the image is still in cache, it shouldn't. My Apache log shows another reality indeed..

Maybe there are some switches to set the cache size?

Heres my litte script:

import React, { Component } from 'react'
import { Image } from 'react-native'
import _ from 'lodash'

// Image Prefetch: https://facebook.github.io/react-native/docs/image.html#prefetch

export default function(uris) {
    prefetch(uris, 0)
}

const prefetch = function(uris, i) {
    const uri = uris[i]
    let startTime = new Date()
    Image.prefetch(uri).then(() => {
        // console.log(`✔ Prefetch OK (+${new Date() - startTime}ms) from ${uri}`)
    }, error => {
        console.log(`✘ Prefetch failed (+${new Date() - startTime}ms) from ${uri}`)
    }).then(() => {
        if (i+2 <= uris.length) {
            prefetch(uris, i+1)
        }
    })
}

BTW: Why doesn't have the iOS simulator an offline mode?! To test it, I must go offline with my Mac.

ronnyhartenstein commented Sep 29, 2016

@ChristianTucker Yes, the offical examples here are a big ball of mud. But still it helps me.

In my scenario I want to prefetch a lot of really small images (~1000 of 1kB) and want to do it on start. So if the prefetch of an image URI is still finished, then <Image ..> should use them from store. Works well, but it prefetches nearly every time. If the image is still in cache, it shouldn't. My Apache log shows another reality indeed..

Maybe there are some switches to set the cache size?

Heres my litte script:

import React, { Component } from 'react'
import { Image } from 'react-native'
import _ from 'lodash'

// Image Prefetch: https://facebook.github.io/react-native/docs/image.html#prefetch

export default function(uris) {
    prefetch(uris, 0)
}

const prefetch = function(uris, i) {
    const uri = uris[i]
    let startTime = new Date()
    Image.prefetch(uri).then(() => {
        // console.log(`✔ Prefetch OK (+${new Date() - startTime}ms) from ${uri}`)
    }, error => {
        console.log(`✘ Prefetch failed (+${new Date() - startTime}ms) from ${uri}`)
    }).then(() => {
        if (i+2 <= uris.length) {
            prefetch(uris, i+1)
        }
    })
}

BTW: Why doesn't have the iOS simulator an offline mode?! To test it, I must go offline with my Mac.

@peacechen

This comment has been minimized.

Show comment
Hide comment
@peacechen

peacechen Nov 8, 2016

@ronnyhartenstein Your example fetches the images in series (loads one, waits until it finishes, then loads the next), which slows down linearly with the number of images. You could run all the prefetches in parallel like this:

var imagePrefetch = [];
for (let uri of uris) {
    imagePrefetch.push(Image.prefetch(uri));
}
Promise.all(imagePrefetch).then(results => {
    console.log("All images prefetched in parallel");
});

This seems to work for the most part, but I've seen it return before the images are fully prefetched if there are a high number of images, or possibly large images. Perhaps they're overflowing the cache causing the first prefetched images to get pushed out?

peacechen commented Nov 8, 2016

@ronnyhartenstein Your example fetches the images in series (loads one, waits until it finishes, then loads the next), which slows down linearly with the number of images. You could run all the prefetches in parallel like this:

var imagePrefetch = [];
for (let uri of uris) {
    imagePrefetch.push(Image.prefetch(uri));
}
Promise.all(imagePrefetch).then(results => {
    console.log("All images prefetched in parallel");
});

This seems to work for the most part, but I've seen it return before the images are fully prefetched if there are a high number of images, or possibly large images. Perhaps they're overflowing the cache causing the first prefetched images to get pushed out?

@ronnyhartenstein

This comment has been minimized.

Show comment
Hide comment
@ronnyhartenstein

ronnyhartenstein Feb 12, 2017

@peacechen Thank you, Promise.all is indeed something I should try.

ronnyhartenstein commented Feb 12, 2017

@peacechen Thank you, Promise.all is indeed something I should try.

@mkozhukharenko

This comment has been minimized.

Show comment
Hide comment
@mkozhukharenko

mkozhukharenko Aug 9, 2017

how to check if image is already prefetched? How to clear prefetch image from a cache?

mkozhukharenko commented Aug 9, 2017

how to check if image is already prefetched? How to clear prefetch image from a cache?

@facebook facebook locked as resolved and limited conversation to collaborators Jul 22, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.