diff --git a/src/components/content/content.ts b/src/components/content/content.ts index b25c411f59..6e9fa18998 100644 --- a/src/components/content/content.ts +++ b/src/components/content/content.ts @@ -342,14 +342,14 @@ export class Content extends Ion implements OnDestroy, OnInit { // emit to all of our other friends things be scrolling this.ionScroll.emit(ev); - this.imgsRefresh(); + this.imgsUpdate(); }); // subscribe to the scroll end this._scroll.scrollEnd.subscribe(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); // initial imgs refresh - this.imgsRefresh(); + this.imgsUpdate(); this.readReady.emit(); } @@ -766,22 +766,30 @@ export class Content extends Ion implements OnDestroy, OnInit { /** * @private */ - imgsRefresh() { - if (this._imgs.length && this.isImgsRefreshable()) { - loadImgs(this._imgs, this.scrollTop, this.scrollHeight, this.directionY, IMG_REQUESTABLE_BUFFER, IMG_RENDERABLE_BUFFER); + imgsUpdate() { + if (this._imgs.length && this.isImgsUpdatable()) { + updateImgs(this._imgs, this.scrollTop, this.scrollHeight, this.directionY, IMG_REQUESTABLE_BUFFER, IMG_RENDERABLE_BUFFER); } } /** * @private */ - isImgsRefreshable() { + isImgsUpdatable() { + // an image is only "updatable" if the content + // isn't scrolling too fast 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 priority1: 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_RENDERABLE_BUFFER = 200; +const IMG_RENDERABLE_BUFFER = 300; function sortTopToBottom(a: Img, b: Img) { diff --git a/src/components/img/img.ts b/src/components/img/img.ts index 244e5eace7..808d925f66 100644 --- a/src/components/img/img.ts +++ b/src/components/img/img.ts @@ -10,86 +10,110 @@ import { Platform } from '../../platform/platform'; /** * @name Img * @description - * Two of the biggest cuprits of scrolling jank is starting up a new - * HTTP request, and rendering images. These two reasons is largely why - * `ion-img` was created and the problems which it is helping to solve. - * The standard `` element is often a large source of these problems, - * and what makes matters worse is that the app does not have fine-grained - * control of each img element. + * Two of the biggest cuprits of scroll jank is starting up a new HTTP + * request, and rendering images. These two reasons is largely why + * `ion-img` was created. The standard HTML `img` element is often a large + * source of these problems, and what makes matters worse is that the app + * does not have fine-grained control of requests and rendering for each + * `img` element. * - * The `ion-img` component is similar to the standard `` element, + * The `ion-img` component is similar to the standard `img` element, * but it also adds features in order to provide improved performance. * Features include only loading images which are visible, using web workers * 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 - * the standard `` element. A good rule is, if there are only a few images - * to be rendered on one page, then the standard `` may be best. However, if - * a page has the potential for hundreds or even thousands of images within a - * scrollable area, then `ion-img` would be better suited for the job. + * Note that `ion-img` also comes with a few more restrictions in comparison + * to the standard `img` element. A good rule is, if there are only a few + * images to be rendered on a page, then the standard `img` is probably + * best. However, if a page has the potential for hundreds or even thousands + * of images within a scrollable area, then `ion-img` would be better suited + * for the job. * * * ### Lazy Loading * * Lazy loading images refers to only loading images which are actually * 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 - * scrolls down, each image which becomes visible is then loaded on-demand. + * not viewable on the initial load would not be downloaded or rendered. Next, + * 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 - * started and valuable bandwidth wasted, and to free up browser resources - * which would be wasted on images which are not even viewable. For example, - * animated GIFs are enourmous performance drains, however, with `ion-img` - * the app is able to dedicate resources to just the viewable images. + * The benefits of this approach is that unnecessary and resource intensive + * HTTP requests are not started, valuable bandwidth isn't wasted, and this + * allows the browser to free up resources which would be wasted on images + * which are not even viewable. For example, animated GIFs are enourmous + * 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 * * 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 - * images which are viewable. Image dimensions can either by set as properties, - * inline styles, or stylesheets. It doesn't matter which method of setting - * dimensions is used, but it's important that somehow each `ion-img` - * has been given an exact size. + * images which are viewable. Image dimensions can either by set as + * properties, inline styles, or external stylesheets. It doesn't matter + * which method of setting dimensions is used, but it's important that somehow + * each `ion-img` has been given an exact size. * * For example, by default `` and `` already come - * with exact sizes when placed within ``. By giving each image an - * exact size, this then further locks in the size of each `ion-item`, which - * again helps improve scroll performance. + * with exact sizes when placed within an ``. By giving each image + * an exact size, this then further locks in the size of each `ion-item`, + * which again helps improve scroll performance. * - * @usage * ```html - * + * * * - * + * * * - * + * * * ``` * + * 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 element’s entire content box. + * Its concrete object size is resolved as a cover constraint against the + * element’s used width and height. + * * * ### Web Worker and XHR Requests * - * Another big cause of scroll jank is kicking off a new HTTP request, which - * is exactly what images do. Normally, this isn't a problem for something like - * a blog since all image HTTP requests are started immediately as HTML - * parses. However, Ionic has the ability to include hundreds to thousands of - * images within one page, but we're not actually loading all of the images at once. + * Another big cause of scroll jank is kicking off a new HTTP request, + * which is exactly what images do. Normally, this isn't a problem for + * something like a blog since all image HTTP requests are started immediately + * as HTML parses. However, Ionic has the ability to include hundreds, or even + * 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 - * images. If they're scrolling extremely fast, the app wouldn't want to start all of - * those requests, but if they're scrolling slowly they would. Additionally, it's - * most browsers can only have six requests at one time for the same domain, so - * it's extemely important that we're managing which images we should downloading. + * Imagine an app where users can scroll slowly, or very quickly, through + * thousands of images. If they're scrolling extremely fast, ideally the app + * wouldn't want to start all of those image requests, but if they're scrolling + * slowly they would. Additionally, most browsers can only have six requests at + * 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 - * lifting to another thread. Not only are able to take the load of the main thread, - * but we're also able to accurately control exactly which images should be - * downloading, along with the ability to abort unnecessary requests. Aborting - * requets is just as important so that Ionic can free up connections for the most - * important images which are visible. + * Next, by running the image request within a web worker, we're able to pass + * off the heavy lifting to another thread. Not only are able to take the load + * of the main thread, but we're also able to accurately control exactly which + * images should be downloading, along with the ability to abort unnecessary + * requests. Aborting requets is just as important so that Ionic can free up + * 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({ @@ -155,7 +179,11 @@ export class Img implements OnDestroy { return this._src; } set src(newSrc: string) { + // if the source hasn't changed, then um, let's not change it if (newSrc !== this._src) { + // we're changing the source + // so abort any active http requests + // and render the image empty this.reset(); // update to the new src @@ -164,6 +192,7 @@ export class Img implements OnDestroy { // reset any existing datauri we might be holding onto this._tmpDataUri = null; + // run update to kick off requests or render if everything is good this.update(); } } @@ -190,16 +219,25 @@ export class Img implements OnDestroy { * @private */ 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) { + // 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()}`); this._requestingSrc = this._src; + // create a callback for when we get data back from the web worker this._cb = (msg: ImgResponseMessage) => { this._loadResponse(msg); }; + // post the message to the web worker this._ldr.load(this._src, this._cache, this._cb); + + // set the dimensions of the image if we do have different data this._setDims(); } @@ -276,9 +314,13 @@ export class Img implements OnDestroy { private _getBounds() { if (this._bounds) { + // we've been manually passed bounds data + // this is probably from Virtual Scroll items return this._bounds; } if (!this._rect) { + // we don't have bounds from virtual scroll + // so let's do the raw DOM lookup w/ getBoundingClientRect this._rect = (this._elementRef.nativeElement).getBoundingClientRect(); 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. * 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() 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 - * 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() 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 - * 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() set height(val: string | number) { @@ -332,6 +376,8 @@ export class Img implements OnDestroy { } 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)) { var wrapperEle: HTMLImageElement = this._elementRef.nativeElement; var renderer = this._renderer; diff --git a/src/components/virtual-scroll/virtual-scroll.ts b/src/components/virtual-scroll/virtual-scroll.ts index 3803e68de9..4f7213c8b8 100644 --- a/src/components/virtual-scroll/virtual-scroll.ts +++ b/src/components/virtual-scroll/virtual-scroll.ts @@ -518,7 +518,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { estimateHeight(recordsLength, cells[cells.length - 1], this._vHeight, 0.25) ); - this._content.imgsRefresh(); + this._content.imgsUpdate(); }); }