Skip to content

Commit 3982c84

Browse files
committed
Lazy Images: Add lazy images module to improve load time
1 parent 82fd002 commit 3982c84

File tree

5 files changed

+269
-4
lines changed

5 files changed

+269
-4
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
_inc/client/**/test/*.js
2+
modules/lazy-images/assets/lazy-images.js

modules/lazy-images.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/**
4+
* Module Name: Lazy Images
5+
* Module Description: Improve performance by loading images just before they scroll into view
6+
* Sort Order: 24
7+
* Recommendation Order: 14
8+
* First Introduced: 5.6.0
9+
* Requires Connection: No
10+
* Auto Activate: No
11+
* Module Tags: Appearance, Recommended
12+
* Feature: Appearance
13+
* Additional Search Queries: mobile, theme, performance
14+
*/
15+
16+
// TODO: Add attribution here
17+
18+
require_once( JETPACK__PLUGIN_DIR . 'modules/lazy-images/lazy-images.php' );
19+
Jetpack_Lazy_Images::instance();
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/* global IntersectionObserver */
2+
3+
/**
4+
* Huge props to deanhume for https://github.com/deanhume/lazy-observer-load
5+
*/
6+
// Get all of the images that are marked up to lazy load
7+
( function() {
8+
var images = document.querySelectorAll( 'img[data-lazy-src]' ),
9+
config = {
10+
// If the image gets within 50px in the Y axis, start the download.
11+
rootMargin: '50px 0px',
12+
threshold: 0.01
13+
},
14+
imageCount = images.length,
15+
observer,
16+
i;
17+
18+
// If we don't have support for intersection observer, loads the images immediately
19+
if ( ! ( 'IntersectionObserver' in window ) ) {
20+
loadImagesImmediately( images );
21+
} else {
22+
// It is supported, load the images
23+
observer = new IntersectionObserver( onIntersection, config );
24+
25+
// foreach() is not supported in IE
26+
for ( i = 0; i < images.length; i++ ) {
27+
var image = images[ i ];
28+
if ( image.classList.contains( 'jetpack-lazy-image--handled' ) ) {
29+
continue;
30+
}
31+
32+
observer.observe( image );
33+
}
34+
}
35+
36+
/**
37+
* Fetchs the image for the given URL
38+
* @param {string} url
39+
*/
40+
function fetchImage( url, callback ) {
41+
var image = new Image();
42+
image.onload = function() {
43+
callback();
44+
};
45+
46+
// An error from loading the image would've loaded
47+
// a broken image anyways.
48+
image.onerror = function() {
49+
callback();
50+
};
51+
52+
image.src = url;
53+
}
54+
55+
/**
56+
* Preloads the image
57+
* @param {object} image
58+
*/
59+
function preloadImage( image ) {
60+
var src = image.dataset.lazySrc,
61+
srcset;
62+
63+
if ( ! src ) {
64+
return;
65+
}
66+
67+
srcset = image.dataset.lazySrcset;
68+
69+
fetchImage( src, function() {
70+
applyImage( image, src, srcset );
71+
} );
72+
}
73+
74+
/**
75+
* Load all of the images immediately
76+
* @param {NodeListOf<Element>} immediateImages List of lazy-loaded images to load immediately.
77+
*/
78+
function loadImagesImmediately( immediateImages ) {
79+
var i;
80+
81+
// foreach() is not supported in IE
82+
for ( i = 0; i < immediateImages.length; i++ ) {
83+
var image = immediateImages[ i ];
84+
preloadImage( image );
85+
}
86+
}
87+
88+
/**
89+
* On intersection
90+
* @param {array} entries List of elements being observed.
91+
*/
92+
function onIntersection( entries ) {
93+
var i;
94+
95+
// Disconnect if we've already loaded all of the images
96+
if ( imageCount === 0 ) {
97+
observer.disconnect();
98+
}
99+
100+
// Loop through the entries
101+
for ( i = 0; i < entries.length; i++ ) {
102+
var entry = entries[ i ];
103+
// Are we in viewport?
104+
if ( entry.intersectionRatio > 0 ) {
105+
imageCount--;
106+
107+
// Stop watching and load the image
108+
observer.unobserve( entry.target );
109+
preloadImage( entry.target );
110+
}
111+
}
112+
}
113+
114+
/**
115+
* Apply the image
116+
* @param {object} img The image object.
117+
* @param {string} src The image source to set.
118+
* @param {string} srcset The image srcset to set.
119+
*/
120+
function applyImage( img, src, srcset ) {
121+
// Prevent this from being lazy loaded a second time.
122+
img.classList.add( 'jetpack-lazy-image--handled' );
123+
img.src = src;
124+
img.srcset = srcset;
125+
img.classList.add( 'fade-in' );
126+
}
127+
} )();
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
class Jetpack_Lazy_Images {
4+
private static $__instance = null;
5+
/**
6+
* Singleton implementation
7+
*
8+
* @return object
9+
*/
10+
public static function instance() {
11+
if ( is_null( self::$__instance ) ) {
12+
self::$__instance = new Jetpack_Lazy_Images();
13+
}
14+
15+
return self::$__instance;
16+
}
17+
18+
/**
19+
* Registers actions
20+
*/
21+
private function __construct() {
22+
// modify content
23+
add_action( 'wp_head', array( $this, 'setup_filters' ), 9999 ); // we don't really want to modify anything in <head> since it's mostly all metadata
24+
25+
// js to do lazy loading
26+
add_action( 'init', array( $this, 'register_assets' ) );
27+
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
28+
}
29+
30+
public function setup_filters() {
31+
add_filter( 'the_content', array( $this, 'add_image_placeholders' ), 99 ); // run this later, so other content filters have run, including image_add_wh on WP.com
32+
add_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), 11 );
33+
add_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), 11 );
34+
}
35+
36+
public function add_image_placeholders( $content ) {
37+
// Don't lazyload for feeds, previews
38+
if ( is_feed() || is_preview() ) {
39+
return $content;
40+
41+
}
42+
43+
// Don't lazy-load if the content has already been run through previously
44+
if ( false !== strpos( $content, 'data-lazy-src' ) ) {
45+
return $content;
46+
}
47+
48+
// This is a pretty simple regex, but it works
49+
$content = preg_replace_callback( '#<(img)([^>]+?)(>(.*?)</\\1>|[\/]?>)#si', array( __CLASS__, 'process_image' ), $content );
50+
51+
return $content;
52+
}
53+
54+
function process_image( $matches ) {
55+
$old_attributes_str = $matches[2];
56+
$old_attributes = wp_kses_hair( $old_attributes_str, wp_allowed_protocols() );
57+
58+
if ( empty( $old_attributes['src'] ) ) {
59+
return $matches[0];
60+
}
61+
62+
$image_src = $old_attributes['src']['value'];
63+
64+
if ( isset( $old_attributes['srcset'] ) ) {
65+
$image_srcset = $old_attributes['srcset']['value'];
66+
} else {
67+
$image_srcset = '';
68+
}
69+
70+
// Remove src, lazy-src, srcset and lazy-srcset since we manually add them
71+
$new_attributes = $old_attributes;
72+
unset( $new_attributes['src'], $new_attributes['srcset'], $new_attributes['data-lazy-src'], $new_attributes['data-lazy-srcset'] );
73+
74+
$new_attributes_str = $this->build_attributes_string( $new_attributes );
75+
76+
return sprintf(
77+
'<img data-lazy-src="%1$s" data-lazy-srcset="%2$s" %3$s><noscript>%4$s</noscript>',
78+
esc_url( $image_src ),
79+
esc_attr( $image_srcset ),
80+
$new_attributes_str,
81+
$matches[0]
82+
);
83+
}
84+
85+
private function build_attributes_string( $attributes ) {
86+
$string = array();
87+
foreach ( $attributes as $name => $attribute ) {
88+
$value = $attribute['value'];
89+
if ( '' === $value ) {
90+
$string[] = sprintf( '%s', $name );
91+
} else {
92+
$string[] = sprintf( '%s="%s"', $name, esc_attr( $value ) );
93+
}
94+
}
95+
return implode( ' ', $string );
96+
}
97+
98+
public function register_assets() {
99+
wp_register_script(
100+
'jetpack-lazy-images',
101+
plugins_url( 'modules/lazy-images/assets/lazy-images.js', JETPACK__PLUGIN_FILE ),
102+
array(),
103+
'1.5',
104+
true
105+
);
106+
}
107+
108+
public function enqueue_assets() {
109+
wp_enqueue_script( 'jetpack-lazy-images' );
110+
}
111+
}

modules/module-headings.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ function jetpack_get_module_i18n( $key ) {
8080
'description' => _x( 'Use LaTeX markup for complex equations and other geekery.', 'Module Description', 'jetpack' ),
8181
),
8282

83+
'lazy-images' => array(
84+
'name' => _x( 'Lazy Images', 'Module Name', 'jetpack' ),
85+
'description' => _x( 'Improve performance by loading images just before they scroll into view', 'Module Description', 'jetpack' ),
86+
),
87+
8388
'likes' => array(
8489
'name' => _x( 'Likes', 'Module Name', 'jetpack' ),
8590
'description' => _x( 'Give visitors an easy way to show they appreciate your content.', 'Module Description', 'jetpack' ),
@@ -286,6 +291,7 @@ function jetpack_get_module_i18n_tag( $key ) {
286291
// - modules/custom-css.php
287292
// - modules/gravatar-hovercards.php
288293
// - modules/infinite-scroll.php
294+
// - modules/lazy-images.php
289295
// - modules/minileven.php
290296
// - modules/photon.php
291297
// - modules/seo-tools.php
@@ -300,11 +306,8 @@ function jetpack_get_module_i18n_tag( $key ) {
300306
// - modules/sso.php
301307
'Developers' =>_x( 'Developers', 'Module Tag', 'jetpack' ),
302308

303-
// Modules with `Centralized Management` tag:
304-
// - modules/manage.php
305-
'Centralized Management' =>_x( 'Centralized Management', 'Module Tag', 'jetpack' ),
306-
307309
// Modules with `Recommended` tag:
310+
// - modules/lazy-images.php
308311
// - modules/manage.php
309312
// - modules/minileven.php
310313
// - modules/monitor.php
@@ -317,6 +320,10 @@ function jetpack_get_module_i18n_tag( $key ) {
317320
// - modules/stats.php
318321
'Recommended' =>_x( 'Recommended', 'Module Tag', 'jetpack' ),
319322

323+
// Modules with `Centralized Management` tag:
324+
// - modules/manage.php
325+
'Centralized Management' =>_x( 'Centralized Management', 'Module Tag', 'jetpack' ),
326+
320327
// Modules with `General` tag:
321328
// - modules/masterbar.php
322329
'General' =>_x( 'General', 'Module Tag', 'jetpack' ),

0 commit comments

Comments
 (0)