docs(img): update img and web worker docs

This commit is contained in:
Adam Bradley
2016-12-07 10:57:13 -06:00
parent ca489e8200
commit 1c52c2dd1f
3 changed files with 114 additions and 60 deletions

View File

@ -342,14 +342,14 @@ export class Content extends Ion implements OnDestroy, OnInit {
// emit to all of our other friends things be scrolling // emit to all of our other friends things be scrolling
this.ionScroll.emit(ev); this.ionScroll.emit(ev);
this.imgsRefresh(); this.imgsUpdate();
}); });
// subscribe to the scroll end // subscribe to the scroll end
this._scroll.scrollEnd.subscribe(ev => { this._scroll.scrollEnd.subscribe(ev => {
this.ionScrollEnd.emit(ev); this.ionScrollEnd.emit(ev);
this.imgsRefresh(); this.imgsUpdate();
}); });
} }
@ -668,7 +668,7 @@ export class Content extends Ion implements OnDestroy, OnInit {
this._scroll.init(this._scrollEle, this._cTop, this._cBottom); this._scroll.init(this._scrollEle, this._cTop, this._cBottom);
// initial imgs refresh // initial imgs refresh
this.imgsRefresh(); this.imgsUpdate();
this.readReady.emit(); this.readReady.emit();
} }
@ -766,22 +766,30 @@ export class Content extends Ion implements OnDestroy, OnInit {
/** /**
* @private * @private
*/ */
imgsRefresh() { imgsUpdate() {
if (this._imgs.length && this.isImgsRefreshable()) { if (this._imgs.length && this.isImgsUpdatable()) {
loadImgs(this._imgs, this.scrollTop, this.scrollHeight, this.directionY, IMG_REQUESTABLE_BUFFER, IMG_RENDERABLE_BUFFER); updateImgs(this._imgs, this.scrollTop, this.scrollHeight, this.directionY, IMG_REQUESTABLE_BUFFER, IMG_RENDERABLE_BUFFER);
} }
} }
/** /**
* @private * @private
*/ */
isImgsRefreshable() { isImgsUpdatable() {
// an image is only "updatable" if the content
// isn't scrolling too fast
return Math.abs(this.velocityY) < 3; return Math.abs(this.velocityY) < 3;
} }
} }
export function loadImgs(imgs: Img[], scrollTop: number, scrollHeight: number, scrollDirectionY: ScrollDirection, requestableBuffer: number, renderableBuffer: number) { export function updateImgs(imgs: Img[], scrollTop: number, scrollHeight: number, scrollDirectionY: ScrollDirection, requestableBuffer: number, renderableBuffer: number) {
// ok, so it's time to see which images, if any, should be requested and rendered
// ultimately, if we're scrolling fast then don't bother requesting or rendering
// when scrolling is done, then it needs to do a check to see which images are
// important to request and render, and which image requests should be aborted.
// Additionally, images which are not near the viewable area should not be
// rendered at all in order to save browser resources.
const scrollBottom = (scrollTop + scrollHeight); const scrollBottom = (scrollTop + scrollHeight);
const priority1: Img[] = []; const priority1: Img[] = [];
const priority2: Img[] = []; const priority2: Img[] = [];
@ -863,7 +871,7 @@ export function loadImgs(imgs: Img[], scrollTop: number, scrollHeight: number, s
} }
const IMG_REQUESTABLE_BUFFER = 1200; const IMG_REQUESTABLE_BUFFER = 1200;
const IMG_RENDERABLE_BUFFER = 200; const IMG_RENDERABLE_BUFFER = 300;
function sortTopToBottom(a: Img, b: Img) { function sortTopToBottom(a: Img, b: Img) {

View File

@ -10,86 +10,110 @@ import { Platform } from '../../platform/platform';
/** /**
* @name Img * @name Img
* @description * @description
* Two of the biggest cuprits of scrolling jank is starting up a new * Two of the biggest cuprits of scroll jank is starting up a new HTTP
* HTTP request, and rendering images. These two reasons is largely why * request, and rendering images. These two reasons is largely why
* `ion-img` was created and the problems which it is helping to solve. * `ion-img` was created. The standard HTML `img` element is often a large
* The standard `<img>` element is often a large source of these problems, * source of these problems, and what makes matters worse is that the app
* and what makes matters worse is that the app does not have fine-grained * does not have fine-grained control of requests and rendering for each
* control of each img element. * `img` element.
* *
* The `ion-img` component is similar to the standard `<img>` element, * The `ion-img` component is similar to the standard `img` element,
* but it also adds features in order to provide improved performance. * but it also adds features in order to provide improved performance.
* Features include only loading images which are visible, using web workers * Features include only loading images which are visible, using web workers
* for HTTP requests, preventing jank while scrolling and in-memory caching. * for HTTP requests, preventing jank while scrolling and in-memory caching.
* *
* Note that `ion-img` also comes with a few more restrictions in comparison to * Note that `ion-img` also comes with a few more restrictions in comparison
* the standard `<img>` element. A good rule is, if there are only a few images * to the standard `img` element. A good rule is, if there are only a few
* to be rendered on one page, then the standard `<img>` may be best. However, if * images to be rendered on a page, then the standard `img` is probably
* a page has the potential for hundreds or even thousands of images within a * best. However, if a page has the potential for hundreds or even thousands
* scrollable area, then `ion-img` would be better suited for the job. * of images within a scrollable area, then `ion-img` would be better suited
* for the job.
* *
* *
* ### Lazy Loading * ### Lazy Loading
* *
* Lazy loading images refers to only loading images which are actually * Lazy loading images refers to only loading images which are actually
* visible within the user's viewport. This also means that images which are * visible within the user's viewport. This also means that images which are
* not viewable on the initial load would not be downloaded. Next, as the user * not viewable on the initial load would not be downloaded or rendered. Next,
* scrolls down, each image which becomes visible is then loaded on-demand. * as the user scrolls, each image which becomes visible is then requested
* then rendered on-demand.
* *
* The benefits of this approach is that unnecessary HTTP requests are not * The benefits of this approach is that unnecessary and resource intensive
* started and valuable bandwidth wasted, and to free up browser resources * HTTP requests are not started, valuable bandwidth isn't wasted, and this
* which would be wasted on images which are not even viewable. For example, * allows the browser to free up resources which would be wasted on images
* animated GIFs are enourmous performance drains, however, with `ion-img` * which are not even viewable. For example, animated GIFs are enourmous
* the app is able to dedicate resources to just the viewable images. * performance drains, however, with `ion-img` the app is able to dedicate
* resources to just the viewable images. But again, if the problems listed
* above are not problems within your app, then the standard `img` element
* may be best.
* *
* *
* ### Image Dimensions * ### Image Dimensions
* *
* By providing image dimensions up front, Ionic is able to accurately size * By providing image dimensions up front, Ionic is able to accurately size
* up the image's location within the viewport, which helps lazy load only * up the image's location within the viewport, which helps lazy load only
* images which are viewable. Image dimensions can either by set as properties, * images which are viewable. Image dimensions can either by set as
* inline styles, or stylesheets. It doesn't matter which method of setting * properties, inline styles, or external stylesheets. It doesn't matter
* dimensions is used, but it's important that somehow each `ion-img` * which method of setting dimensions is used, but it's important that somehow
* has been given an exact size. * each `ion-img` has been given an exact size.
* *
* For example, by default `<ion-avatar>` and `<ion-thumbnail>` already come * For example, by default `<ion-avatar>` and `<ion-thumbnail>` already come
* with exact sizes when placed within `<ion-item>`. By giving each image an * with exact sizes when placed within an `<ion-item>`. By giving each image
* exact size, this then further locks in the size of each `ion-item`, which * an exact size, this then further locks in the size of each `ion-item`,
* again helps improve scroll performance. * which again helps improve scroll performance.
* *
* @usage
* ```html * ```html
* <!-- set using plain attributes --> * <!-- dimensions set using attributes -->
* <ion-img width="80" height="80" src="..."></ion-img> * <ion-img width="80" height="80" src="..."></ion-img>
* *
* <!-- bind using properties --> * <!-- dimensions set using input properties -->
* <ion-img [width]="imgWidth" [height]="imgHeight" src="..."></ion-img> * <ion-img [width]="imgWidth" [height]="imgHeight" src="..."></ion-img>
* *
* <!-- inline styles --> * <!-- dimensions set using inline styles -->
* <ion-img style="width: 80px; height: 80px;" src="..."></ion-img> * <ion-img style="width: 80px; height: 80px;" src="..."></ion-img>
* ``` * ```
* *
* Additionally, each `ion-img` uses the `object-fit: cover` CSS property.
* What this means is that the actual rendered image will center itself within
* it's container. Or to really get detailed: The image is sized to maintain
* its aspect ratio while filling the containing elements entire content box.
* Its concrete object size is resolved as a cover constraint against the
* elements used width and height.
*
* *
* ### Web Worker and XHR Requests * ### Web Worker and XHR Requests
* *
* Another big cause of scroll jank is kicking off a new HTTP request, which * Another big cause of scroll jank is kicking off a new HTTP request,
* is exactly what images do. Normally, this isn't a problem for something like * which is exactly what images do. Normally, this isn't a problem for
* a blog since all image HTTP requests are started immediately as HTML * something like a blog since all image HTTP requests are started immediately
* parses. However, Ionic has the ability to include hundreds to thousands of * as HTML parses. However, Ionic has the ability to include hundreds, or even
* images within one page, but we're not actually loading all of the images at once. * thousands of images within one page, but its not actually loading all of
* the images at the same time.
* *
* Imagine an app where users can slowly, or quickly, scroll through hundreds of * Imagine an app where users can scroll slowly, or very quickly, through
* images. If they're scrolling extremely fast, the app wouldn't want to start all of * thousands of images. If they're scrolling extremely fast, ideally the app
* those requests, but if they're scrolling slowly they would. Additionally, it's * wouldn't want to start all of those image requests, but if they're scrolling
* most browsers can only have six requests at one time for the same domain, so * slowly they would. Additionally, most browsers can only have six requests at
* it's extemely important that we're managing which images we should downloading. * one time for the same domain, so it's extemely important that we're managing
* exacctly which images we should downloading. Basically we want to ensure
* that the app is requesting the most important images, and aborting
* unnecessary requests, which is another benefit of using `ion-img`.
* *
* By place XMLHttpRequest within a web worker, we're able to pass off the heavy * Next, by running the image request within a web worker, we're able to pass
* lifting to another thread. Not only are able to take the load of the main thread, * off the heavy lifting to another thread. Not only are able to take the load
* but we're also able to accurately control exactly which images should be * of the main thread, but we're also able to accurately control exactly which
* downloading, along with the ability to abort unnecessary requests. Aborting * images should be downloading, along with the ability to abort unnecessary
* requets is just as important so that Ionic can free up connections for the most * requests. Aborting requets is just as important so that Ionic can free up
* important images which are visible. * connections for the most important images which are visible.
*
* One restriction however, is that all image requests must work with
* [cross-origin HTTP requests (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS).
* Traditionally, the `img` element does not have this issue, but because
* `ion-img` uses `XMLHttpRequest` within a web worker, then requests for
* images must be served from the same domain, or the image server's response
* must set the `Access-Control-Allow-Origin` HTTP header. Again, if your app
* does not have the same problems which `ion-img` is solving, then it's
* recommended to just use the standard `img` HTML element instead.
* *
*/ */
@Component({ @Component({
@ -155,7 +179,11 @@ export class Img implements OnDestroy {
return this._src; return this._src;
} }
set src(newSrc: string) { set src(newSrc: string) {
// if the source hasn't changed, then um, let's not change it
if (newSrc !== this._src) { if (newSrc !== this._src) {
// we're changing the source
// so abort any active http requests
// and render the image empty
this.reset(); this.reset();
// update to the new src // update to the new src
@ -164,6 +192,7 @@ export class Img implements OnDestroy {
// reset any existing datauri we might be holding onto // reset any existing datauri we might be holding onto
this._tmpDataUri = null; this._tmpDataUri = null;
// run update to kick off requests or render if everything is good
this.update(); this.update();
} }
} }
@ -190,16 +219,25 @@ export class Img implements OnDestroy {
* @private * @private
*/ */
update() { update() {
if (this._src && this._content.isImgsRefreshable()) { // only attempt an update if there is an active src
// and the content containing the image considers it updatable
if (this._src && this._content.isImgsUpdatable()) {
if (this.canRequest && (this._src !== this._renderedSrc && this._src !== this._requestingSrc) && !this._tmpDataUri) { if (this.canRequest && (this._src !== this._renderedSrc && this._src !== this._requestingSrc) && !this._tmpDataUri) {
// only begin the request if we "can" request
// begin the image request if the src is different from the rendered src
// and if we don't already has a tmpDataUri
console.debug(`request ${this._src} ${Date.now()}`); console.debug(`request ${this._src} ${Date.now()}`);
this._requestingSrc = this._src; this._requestingSrc = this._src;
// create a callback for when we get data back from the web worker
this._cb = (msg: ImgResponseMessage) => { this._cb = (msg: ImgResponseMessage) => {
this._loadResponse(msg); this._loadResponse(msg);
}; };
// post the message to the web worker
this._ldr.load(this._src, this._cache, this._cb); this._ldr.load(this._src, this._cache, this._cb);
// set the dimensions of the image if we do have different data
this._setDims(); this._setDims();
} }
@ -276,9 +314,13 @@ export class Img implements OnDestroy {
private _getBounds() { private _getBounds() {
if (this._bounds) { if (this._bounds) {
// we've been manually passed bounds data
// this is probably from Virtual Scroll items
return this._bounds; return this._bounds;
} }
if (!this._rect) { if (!this._rect) {
// we don't have bounds from virtual scroll
// so let's do the raw DOM lookup w/ getBoundingClientRect
this._rect = (<HTMLElement>this._elementRef.nativeElement).getBoundingClientRect(); this._rect = (<HTMLElement>this._elementRef.nativeElement).getBoundingClientRect();
console.debug(`img, ${this._src}, read, ${this._rect.top} - ${this._rect.bottom}`); console.debug(`img, ${this._src}, read, ${this._rect.top} - ${this._rect.bottom}`);
} }
@ -288,7 +330,7 @@ export class Img implements OnDestroy {
/** /**
* @input {any} Sets the bounding rectangle of the element relative to the viewport. * @input {any} Sets the bounding rectangle of the element relative to the viewport.
* When using `VirtualScroll`, each virtual item should pass its bounds to each * When using `VirtualScroll`, each virtual item should pass its bounds to each
* `ion-img`. * `ion-img`. The passed in data object should include `top` and `bottom` properties.
*/ */
@Input() @Input()
set bounds(b: any) { set bounds(b: any) {
@ -313,7 +355,8 @@ export class Img implements OnDestroy {
/** /**
* @input {string} Image width. If this property is not set it's important that * @input {string} Image width. If this property is not set it's important that
* the dimensions are still set using CSS. * the dimensions are still set using CSS. If the dimension is just a number it
* will assume the `px` unit.
*/ */
@Input() @Input()
set width(val: string | number) { set width(val: string | number) {
@ -323,7 +366,8 @@ export class Img implements OnDestroy {
/** /**
* @input {string} Image height. If this property is not set it's important that * @input {string} Image height. If this property is not set it's important that
* the dimensions are still set using CSS. * the dimensions are still set using CSS. If the dimension is just a number it
* will assume the `px` unit.
*/ */
@Input() @Input()
set height(val: string | number) { set height(val: string | number) {
@ -332,6 +376,8 @@ export class Img implements OnDestroy {
} }
private _setDims() { private _setDims() {
// only set the dimensions if we can render
// and only if the dimensions have changed from when we last set it
if (this.canRender && (this._w !== this._wQ || this._h !== this._hQ)) { if (this.canRender && (this._w !== this._wQ || this._h !== this._hQ)) {
var wrapperEle: HTMLImageElement = this._elementRef.nativeElement; var wrapperEle: HTMLImageElement = this._elementRef.nativeElement;
var renderer = this._renderer; var renderer = this._renderer;

View File

@ -518,7 +518,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
estimateHeight(recordsLength, cells[cells.length - 1], this._vHeight, 0.25) estimateHeight(recordsLength, cells[cells.length - 1], this._vHeight, 0.25)
); );
this._content.imgsRefresh(); this._content.imgsUpdate();
}); });
} }