Skip to content
Snippets Groups Projects
Commit 52b8a0db authored by Tim Zallmann's avatar Tim Zallmann Committed by Jacob Schatz
Browse files

Resolve "Lazy load images on the Frontend"

parent 3a26bce8
No related branches found
No related tags found
No related merge requests found
Showing
with 180 additions and 25 deletions
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
 
import './lib/utils/common_utils';
import { placeholderImage } from './lazy_loader';
 
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
Loading
Loading
@@ -56,6 +57,11 @@ const gfmRules = {
return text;
},
},
ImageLazyLoadFilter: {
'img'(el, text) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
},
},
VideoLinkFilter: {
'.video-container'(el) {
const videoEl = el.querySelector('video');
Loading
Loading
@@ -163,7 +169,9 @@ const gfmRules = {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
},
'img'(el) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
const imageSrc = el.src;
const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || '');
return `![${el.getAttribute('alt')}](${imageUrl})`;
},
'a.anchor'(el, text) {
// Don't render a Markdown link for the anchor link inside a heading
Loading
Loading
/* eslint-disable one-export, one-var, one-var-declaration-per-line */
import _ from 'underscore';
export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
const SCROLL_THRESHOLD = 300;
export default class LazyLoader {
constructor(options = {}) {
this.lazyImages = [];
this.observerNode = options.observerNode || '#content-body';
const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
window.addEventListener('scroll', throttledScrollCheck);
window.addEventListener('resize', debouncedElementsInView);
const scrollContainer = options.scrollContainer || window;
scrollContainer.addEventListener('load', () => this.loadCheck());
}
searchLazyImages() {
this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
this.checkElementsInView();
}
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
if (contentNode) {
const observer = new MutationObserver(() => this.searchLazyImages());
observer.observe(contentNode, {
childList: true,
subtree: true,
});
}
}
loadCheck() {
this.searchLazyImages();
this.startContentObserver();
}
scrollCheck() {
requestAnimationFrame(() => this.checkElementsInView());
}
checkElementsInView() {
const scrollTop = pageYOffset;
const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
let imgBoundRect, imgTop, imgBound;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) {
imgBoundRect = selectedImage.getBoundingClientRect();
imgTop = scrollTop + imgBoundRect.top;
imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) {
LazyLoader.loadImage(selectedImage);
return false;
}
return true;
}
return false;
});
}
static loadImage(img) {
if (img.getAttribute('data-src')) {
img.setAttribute('src', img.getAttribute('data-src'));
img.removeAttribute('data-src');
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
}
}
}
Loading
Loading
@@ -109,6 +109,7 @@ import './label_manager';
import './labels';
import './labels_select';
import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
import './member_expiration_date';
Loading
Loading
@@ -166,6 +167,11 @@ window.addEventListener('load', function onLoad() {
gl.utils.handleLocationHash();
}, false);
 
gl.lazyLoader = new LazyLoader({
scrollContainer: window,
observerNode: '#content-body'
});
$(function () {
var $body = $('body');
var $document = $(document);
Loading
Loading
Loading
Loading
@@ -35,6 +35,8 @@
width: 40px;
height: 40px;
padding: 0;
background: $avatar-background;
overflow: hidden;
 
&.avatar-inline {
float: none;
Loading
Loading
Loading
Loading
@@ -11,8 +11,17 @@
}
 
img {
max-width: 100%;
/*max-width: 100%;*/
margin: 0 0 8px;
min-width: 200px;
min-height: 100px;
background-color: $gray-lightest;
}
img.js-lazy-loaded {
min-width: none;
min-height: none;
background-color: none;
}
 
p a:not(.no-attachment-icon) img {
Loading
Loading
Loading
Loading
@@ -379,7 +379,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
* Avatar
*/
$avatar_radius: 50%;
$avatar-border: $border-color;
$avatar-border: $gray-normal;
$avatar-border-hover: $gray-darker;
$avatar-background: $gray-lightest;
$gl-avatar-size: 40px;
 
/*
Loading
Loading
Loading
Loading
@@ -11,17 +11,12 @@ module AvatarsHelper
def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
css_class = options[:css_class] || ''
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
data_attributes = { container: 'body' }
 
if options[:lazy]
data_attributes[:src] = avatar_url
end
image_tag(
options[:lazy] ? '' : avatar_url,
class: "avatar has-tooltip s#{avatar_size} #{css_class}",
avatar_url,
class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
alt: "#{user_name}'s avatar",
title: user_name,
data: data_attributes
Loading
Loading
Loading
Loading
@@ -61,8 +61,8 @@ module EmailsHelper
else
image_tag(
image_url('mailers/gitlab_header_logo.gif'),
size: "55x50",
alt: "GitLab"
size: '55x50',
alt: 'GitLab'
)
end
end
Loading
Loading
module LazyImageTagHelper
def placeholder_image
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
end
# Override the default ActionView `image_tag` helper to support lazy-loading
def image_tag(source, options = {})
options = options.symbolize_keys
unless options.delete(:lazy) == false
options[:data] ||= {}
options[:data][:src] = path_to_image(source)
options[:class] ||= ""
options[:class] << " lazy"
source = placeholder_image
end
super(source, options)
end
# Required for Banzai::Filter::ImageLazyLoadFilter
module_function :placeholder_image
end
Loading
Loading
@@ -2,7 +2,7 @@ module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge'
image_tag image_url, class: 'js-version-status-badge', lazy: false
end
end
end
Loading
Loading
@@ -11,7 +11,7 @@ module CacheMarkdownField
extend ActiveSupport::Concern
 
# Increment this number every time the renderer changes its output
CACHE_VERSION = 1
CACHE_VERSION = 2
 
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
Loading
Loading
.file-content.image_file
%img{ src: blob_raw_url, alt: viewer.blob.name }
%img{ 'data-src': blob_raw_url, alt: viewer.blob.name }
Loading
Loading
@@ -8,7 +8,7 @@
.image
%span.wrap
.frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
%img{ src: blob_raw_path, alt: diff_file.file_path }
%img{ 'data-src': blob_raw_path, alt: diff_file.file_path }
%p.image-info= number_to_human_size(blob.size)
- else
.image
Loading
Loading
@@ -16,7 +16,7 @@
%span.wrap
.frame.deleted
%a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
%img{ src: old_blob_raw_path, alt: diff_file.old_path }
%img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
%p.image-info.hide
%span.meta-filesize= number_to_human_size(old_blob.size)
|
Loading
Loading
@@ -28,7 +28,7 @@
%span.wrap
.frame.added
%a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) }
%img{ src: blob_raw_path, alt: diff_file.new_path }
%img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
%p.image-info.hide
%span.meta-filesize= number_to_human_size(blob.size)
|
Loading
Loading
@@ -41,10 +41,10 @@
.swipe.view.hide
.swipe-frame
.frame.deleted
%img{ src: old_blob_raw_path, alt: diff_file.old_path }
%img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
.swipe-wrap
.frame.added
%img{ src: blob_raw_path, alt: diff_file.new_path }
%img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
%span.swipe-bar
%span.top-handle
%span.bottom-handle
Loading
Loading
@@ -52,9 +52,9 @@
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
%img{ src: old_blob_raw_path, alt: diff_file.old_path }
%img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
.frame.added
%img{ src: blob_raw_path, alt: diff_file.new_path }
%img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
.controls
.transparent
.drag-track
Loading
Loading
---
title: Lazy load images for better Frontend performance
merge_request: 12503
author:
Loading
Loading
@@ -23,6 +23,18 @@ controlled by the server.
1. The backend code will most likely be using etags. You do not and should not check for status
`304 Not Modified`. The browser will transform it for you.
 
### Lazy Loading
To improve the time to first render we are using lazy loading for images. This works by setting
the actual image source on the `data-src` attribute. After the HTML is rendered and JavaScript is loaded,
the value of `data-src` will be moved to `src` automatically if the image is in the current viewport.
* Prepare images in HTML for lazy loading by renaming the `src` attribute to `data-src`
* If you are using the Rails `image_tag` helper, all images will be lazy-loaded by default unless `lazy: false` is provided.
If you are asynchronously adding content which contains lazy images then you need to call the function
`gl.lazyLoader.searchLazyImages()` which will search for lazy images and load them if needed.
## Reducing Asset Footprint
 
### Page-specific JavaScript
Loading
Loading
Loading
Loading
@@ -114,7 +114,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
 
step 'Image should be shown on the page' do
expect(page).to have_xpath("//img[@src=\"image.jpg\"]")
expect(page).to have_xpath("//img[@data-src=\"image.jpg\"]")
end
 
step 'I click on image link' do
Loading
Loading
Loading
Loading
@@ -118,7 +118,7 @@ module Banzai
end
 
if path
content_tag(:img, nil, src: path, class: 'gfm')
content_tag(:img, nil, data: { src: path }, class: 'gfm')
end
end
 
Loading
Loading
module Banzai
module Filter
# HTML filter that moves the value of the src attribute to the data-src attribute so it can be lazy loaded
class ImageLazyLoadFilter < HTML::Pipeline::Filter
def call
doc.xpath('descendant-or-self::img').each do |img|
img['class'] ||= '' << 'lazy'
img['data-src'] = img['src']
img['src'] = LazyImageTagHelper.placeholder_image
end
doc
end
end
end
end
Loading
Loading
@@ -10,7 +10,7 @@ module Banzai
link = doc.document.create_element(
'a',
class: 'no-attachment-icon',
href: img['src'],
href: img['data-src'] || img['src'],
target: '_blank',
rel: 'noopener noreferrer'
)
Loading
Loading
Loading
Loading
@@ -22,6 +22,7 @@ module Banzai
 
doc.css('img, video').each do |el|
process_link_attr el.attribute('src')
process_link_attr el.attribute('data-src')
end
 
doc
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment