Skip to content

Commit

Permalink
[Image] Add a way to prefetch remote images to cache with Image.prefetch
Browse files Browse the repository at this point in the history
Adds `Image.prefetch` to prefetch remote images before they are used in an actual `Image` component.

Test Plan:
- Image demo in UIExplorer on Android and iOS. Performance is better on iOS but in both cases you can see that loading a prefetched image is faster than a non-prefetched one.
- Using this in production
- CI
  • Loading branch information
ide committed Apr 2, 2016
1 parent 144dc30 commit 447f45d
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 9 deletions.
27 changes: 24 additions & 3 deletions Examples/UIExplorer/ImageExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ var {
var base64Icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACSR7JhAAADtUlEQVR4Ac3YA2Bj6QLH0XPT1Fzbtm29tW3btm3bfLZtv7e2ObZnms7d8Uw098tuetPzrxv8wiISrtVudrG2JXQZ4VOv+qUfmqCGGl1mqLhoA52oZlb0mrjsnhKpgeUNEs91Z0pd1kvihA3ULGVHiQO2narKSHKkEMulm9VgUyE60s1aWoMQUbpZOWE+kaqs4eLEjdIlZTcFZB0ndc1+lhB1lZrIuk5P2aib1NBpZaL+JaOGIt0ls47SKzLC7CqrlGF6RZ09HGoNy1lYl2aRSWL5GuzqWU1KafRdoRp0iOQEiDzgZPnG6DbldcomadViflnl/cL93tOoVbsOLVM2jylvdWjXolWX1hmfZbGR/wjypDjFLSZIRov09BgYmtUqPQPlQrPapecLgTIy0jMgPKtTeob2zWtrGH3xvjUkPCtNg/tm1rjwrMa+mdUkPd3hWbH0jArPGiU9ufCsNNWFZ40wpwn+62/66R2RUtoso1OB34tnLOcy7YB1fUdc9e0q3yru8PGM773vXsuZ5YIZX+5xmHwHGVvlrGPN6ZSiP1smOsMMde40wKv2VmwPPVXNut4sVpUreZiLBHi0qln/VQeI/LTMYXpsJtFiclUN+5HVZazim+Ky+7sAvxWnvjXrJFneVtLWLyPJu9K3cXLWeOlbMTlrIelbMDlrLenrjEQOtIF+fuI9xRp9ZBFp6+b6WT8RrxEpdK64BuvHgDk+vUy+b5hYk6zfyfs051gRoNO1usU12WWRWL73/MMEy9pMi9qIrR4ZpV16Rrvduxazmy1FSvuFXRkqTnE7m2kdb5U8xGjLw/spRr1uTov4uOgQE+0N/DvFrG/Jt7i/FzwxbA9kDanhf2w+t4V97G8lrT7wc08aA2QNUkuTfW/KimT01wdlfK4yEw030VfT0RtZbzjeMprNq8m8tnSTASrTLti64oBNdpmMQm0eEwvfPwRbUBywG5TzjPCsdwk3IeAXjQblLCoXnDVeoAz6SfJNk5TTzytCNZk/POtTSV40NwOFWzw86wNJRpubpXsn60NJFlHeqlYRbslqZm2jnEZ3qcSKgm0kTli3zZVS7y/iivZTweYXJ26Y+RTbV1zh3hYkgyFGSTKPfRVbRqWWVReaxYeSLarYv1Qqsmh1s95S7G+eEWK0f3jYKTbV6bOwepjfhtafsvUsqrQvrGC8YhmnO9cSCk3yuY984F1vesdHYhWJ5FvASlacshUsajFt2mUM9pqzvKGcyNJW0arTKN1GGGzQlH0tXwLDgQTurS8eIQAAAABJRU5ErkJggg==';

var ImageCapInsetsExample = require('./ImageCapInsetsExample');
const IMAGE_PREFETCH_URL = 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1&t=' + Date.now();
var prefetchTask = Image.prefetch(IMAGE_PREFETCH_URL);

var NetworkImageCallbackExample = React.createClass({
getInitialState: function() {
return {
events: [],
startLoadPrefetched: false,
mountTime: new Date(),
};
},
Expand All @@ -57,9 +60,26 @@ var NetworkImageCallbackExample = React.createClass({
style={[styles.base, {overflow: 'visible'}]}
onLoadStart={() => this._loadEventFired(`✔ onLoadStart (+${new Date() - mountTime}ms)`)}
onLoad={() => this._loadEventFired(`✔ onLoad (+${new Date() - mountTime}ms)`)}
onLoadEnd={() => this._loadEventFired(`✔ onLoadEnd (+${new Date() - mountTime}ms)`)}
onLoadEnd={() => {
this._loadEventFired(`✔ onLoadEnd (+${new Date() - mountTime}ms)`);
this.setState({startLoadPrefetched: true}, () => {
prefetchTask.then(() => {
this._loadEventFired(`✔ Prefetch OK (+${new Date() - mountTime}ms)`);
}, error => {
this._loadEventFired(`✘ Prefetch failed (+${new Date() - mountTime}ms)`);
});
});
}}
/>

{this.state.startLoadPrefetched ?
<Image
source={this.props.prefetchedSource}
style={[styles.base, {overflow: 'visible'}]}
onLoadStart={() => this._loadEventFired(`✔ (prefetched) onLoadStart (+${new Date() - mountTime}ms)`)}
onLoad={() => this._loadEventFired(`✔ (prefetched) onLoad (+${new Date() - mountTime}ms)`)}
onLoadEnd={() => this._loadEventFired(`✔ (prefetched) onLoadEnd (+${new Date() - mountTime}ms)`)}
/>
: null}
<Text style={{marginTop: 20}}>
{this.state.events.join('\n')}
</Text>
Expand Down Expand Up @@ -172,7 +192,8 @@ exports.examples = [
title: 'Image Loading Events',
render: function() {
return (
<NetworkImageCallbackExample source={{uri: 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1'}}/>
<NetworkImageCallbackExample source={{uri: 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1&t=' + Date.now()}}
prefetchedSource={{uri: IMAGE_PREFETCH_URL}}/>
);
},
},
Expand Down
12 changes: 11 additions & 1 deletion Libraries/Image/Image.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ var StyleSheetPropType = require('StyleSheetPropType');
var View = require('View');

var flattenStyle = require('flattenStyle');
var invariant = require('fbjs/lib/invariant');
var merge = require('merge');
var requireNativeComponent = require('requireNativeComponent');
var resolveAssetSource = require('resolveAssetSource');

var {
ImageLoader,
} = NativeModules;

/**
* <Image> - A react component for displaying different types of images,
* including network images, static resources, temporary local images, and
Expand Down Expand Up @@ -110,6 +113,13 @@ var Image = React.createClass({

statics: {
resizeMode: ImageResizeMode,
/**
* Prefetches a remote image for later use by downloading it to the disk
* cache
*/
prefetch(url: string) {
return ImageLoader.prefetchImage(url);
},
},

mixins: [NativeMethodsMixin],
Expand Down
16 changes: 11 additions & 5 deletions Libraries/Image/Image.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,22 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType');
var ImageResizeMode = require('ImageResizeMode');
var ImageStylePropTypes = require('ImageStylePropTypes');
var NativeMethodsMixin = require('NativeMethodsMixin');
var NativeModules = require('NativeModules');
var PropTypes = require('ReactPropTypes');
var React = require('React');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
var View = require('View');
var StyleSheet = require('StyleSheet');
var StyleSheetPropType = require('StyleSheetPropType');

var flattenStyle = require('flattenStyle');
var invariant = require('fbjs/lib/invariant');
var requireNativeComponent = require('requireNativeComponent');
var resolveAssetSource = require('resolveAssetSource');
var warning = require('fbjs/lib/warning');

var {
ImageLoader,
ImageViewManager,
NetworkImageViewManager,
} = require('NativeModules');
} = NativeModules;

/**
* A React component for displaying different types of images,
Expand Down Expand Up @@ -181,7 +180,14 @@ var Image = React.createClass({
ImageViewManager.getSize(uri, success, failure || function() {
console.warn('Failed to get size for image: ' + uri);
});
}
},
/**
* Prefetches a remote image for later use by downloading it to the disk
* cache
*/
prefetch(url: string) {
return ImageLoader.prefetchImage(url);
},
},

mixins: [NativeMethodsMixin],
Expand Down
24 changes: 24 additions & 0 deletions Libraries/Image/RCTImageLoader.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
#import "RCTNetworking.h"
#import "RCTUtils.h"

static NSString *const RCTErrorInvalidURI = @"E_INVALID_URI";
static NSString *const RCTErrorPrefetchFailure = @"E_PREFETCH_FAILURE";

@implementation UIImage (React)

- (CAKeyframeAnimation *)reactKeyframeAnimation
Expand Down Expand Up @@ -634,6 +637,27 @@ - (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag
}];
}

#pragma mark - Bridged methods

RCT_EXPORT_METHOD(prefetchImage:(NSString *)uri
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
if (!uri.length) {
reject(RCTErrorInvalidURI, @"Cannot prefetch an image for an empty URI", nil);
return;
}

[_bridge.imageLoader loadImageWithTag:uri callback:^(NSError *error, UIImage *image) {
if (error) {
reject(RCTErrorPrefetchFailure, nil, error);
return;
}

resolve(@YES);
}];
}

#pragma mark - RCTURLRequestHandler

- (BOOL)canHandleRequest:(NSURLRequest *)request
Expand Down
21 changes: 21 additions & 0 deletions ReactAndroid/src/main/java/com/facebook/react/modules/image/BUCK
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
include_defs('//ReactAndroid/DEFS')

android_library(
name = 'image',
srcs = glob(['*.java']),
deps = [
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/common:common'),
react_native_dep('libraries/fresco/fresco-react-native:fbcore'),
react_native_dep('libraries/fresco/fresco-react-native:fresco-react-native'),
react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'),
react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'),
],
visibility = [
'PUBLIC',
],
)

project_config(
src_target = ':image',
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

package com.facebook.react.modules.image;

import android.net.Uri;

import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.datasource.BaseDataSubscriber;
import com.facebook.datasource.DataSource;
import com.facebook.datasource.DataSubscriber;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

public class ImageLoaderModule extends ReactContextBaseJavaModule {

private static final String ERROR_INVALID_URI = "E_INVALID_URI";
private static final String ERROR_PREFETCH_FAILURE = "E_PREFETCH_FAILURE";

public ImageLoaderModule(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public String getName() {
return "ImageLoader";
}

/**
* Prefetches the given image to the Fresco image disk cache.
*
* @param uriString the URI of the remote image to prefetch
* @param promise the promise that is fulfilled when the image is successfully prefetched
* or rejected when there is an error
*/
@ReactMethod
public void prefetchImage(String uriString, final Promise promise) {
if (uriString == null || uriString.isEmpty()) {
promise.reject(ERROR_INVALID_URI, "Cannot prefetch an image for an empty URI");
return;
}

Uri uri = Uri.parse(uriString);
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri).build();

DataSource<Void> prefetchSource = Fresco.getImagePipeline().prefetchToDiskCache(request, this);
DataSubscriber<Void> prefetchSubscriber = new BaseDataSubscriber<Void>() {
@Override
protected void onNewResultImpl(DataSource<Void> dataSource) {
if (!dataSource.isFinished()) {
return;
}
promise.resolve(true);
dataSource.close();
}

@Override
protected void onFailureImpl(DataSource<Void> dataSource) {
promise.reject(ERROR_PREFETCH_FAILURE, dataSource.getFailureCause());
dataSource.close();
}
};
prefetchSource.subscribe(prefetchSubscriber, CallerThreadExecutor.getInstance());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.facebook.react.modules.datepicker.DatePickerDialogModule;
import com.facebook.react.modules.dialog.DialogModule;
import com.facebook.react.modules.fresco.FrescoModule;
import com.facebook.react.modules.image.ImageLoaderModule;
import com.facebook.react.modules.intent.IntentModule;
import com.facebook.react.modules.location.LocationModule;
import com.facebook.react.modules.netinfo.NetInfoModule;
Expand Down Expand Up @@ -75,6 +76,7 @@ public List<NativeModule> createNativeModules(ReactApplicationContext reactConte
new DialogModule(reactContext),
new FrescoModule(reactContext),
new ImageEditingManager(reactContext),
new ImageLoaderModule(reactContext),
new ImageStoreManager(reactContext),
new IntentModule(reactContext),
new LocationModule(reactContext),
Expand Down

0 comments on commit 447f45d

Please sign in to comment.