mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-21 21:15:24 +08:00
docs(img): update img and web worker docs
This commit is contained in:
@ -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) {
|
||||||
|
@ -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 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
|
* ### 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;
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user