blob: 2fd026a617ddd842b529474ffa18d71bf96d8bd0 [file] [log] [blame]
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Author: nikhilmadan@google.com (Nikhil Madan)
#include "net/instaweb/rewriter/public/lazyload_images_filter.h"
#include "base/logging.h"
#include "net/instaweb/http/public/log_record.h"
#include "net/instaweb/rewriter/public/critical_images_finder.h"
#include "net/instaweb/rewriter/public/request_properties.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/static_asset_manager.h"
#include "net/instaweb/util/public/fallback_property_page.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/html/html_element.h"
#include "pagespeed/kernel/html/html_name.h"
#include "pagespeed/kernel/html/html_node.h"
#include "pagespeed/kernel/http/data_url.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/opt/logging/enums.pb.h"
namespace net_instaweb {
namespace {
const char kTrue[] = "true";
const char kFalse[] = "false";
const char kJquerySlider[] = "jquery.sexyslider";
} // namespace
const char* LazyloadImagesFilter::kImageOnloadCode =
"pagespeed.lazyLoadImages.loadIfVisibleAndMaybeBeacon(this);";
const char* LazyloadImagesFilter::kLoadAllImages =
"pagespeed.lazyLoadImages.loadAllImages();";
const char* LazyloadImagesFilter::kOverrideAttributeFunctions =
"pagespeed.lazyLoadImages.overrideAttributeFunctions();";
const char* LazyloadImagesFilter::kIsLazyloadScriptInsertedPropertyName =
"is_lazyload_script_inserted";
LazyloadImagesFilter::LazyloadImagesFilter(RewriteDriver* driver)
: CommonFilter(driver) {
Clear();
blank_image_url_ = GetBlankImageSrc(
driver->options(),
driver->server_context()->static_asset_manager());
}
LazyloadImagesFilter::~LazyloadImagesFilter() {}
void LazyloadImagesFilter::DetermineEnabled(GoogleString* disabled_reason) {
RewriterHtmlApplication::Status should_apply = ShouldApply(driver());
set_is_enabled(should_apply == RewriterHtmlApplication::ACTIVE);
if (!driver()->flushing_early()) {
driver()->log_record()->LogRewriterHtmlStatus(
RewriteOptions::FilterId(RewriteOptions::kLazyloadImages),
should_apply);
}
}
void LazyloadImagesFilter::StartDocumentImpl() {
Clear();
}
void LazyloadImagesFilter::EndDocument() {
// TODO(jmaessen): Fix filter to insert this script
// conditionally.
driver()->UpdatePropertyValueInDomCohort(
driver()->fallback_property_page(),
kIsLazyloadScriptInsertedPropertyName,
main_script_inserted_ ? "1" : "0");
}
void LazyloadImagesFilter::Clear() {
skip_rewrite_ = NULL;
head_element_ = NULL;
main_script_inserted_ = false;
abort_rewrite_ = false;
abort_script_inserted_ = false;
num_images_lazily_loaded_ = 0;
}
RewriterHtmlApplication::Status LazyloadImagesFilter::ShouldApply(
RewriteDriver* driver) {
// Note: there's similar UA logic in
// DedupInlinedImagedFilter::DetermineEnabled, so if this logic changes that
// logic may well require alteration too.
if (!driver->request_properties()->SupportsLazyloadImages()) {
return RewriterHtmlApplication::USER_AGENT_NOT_SUPPORTED;
}
if (driver->flushing_early() ||
(driver->request_headers() != NULL &&
driver->request_headers()->IsXmlHttpRequest())) {
return RewriterHtmlApplication::DISABLED;
}
CriticalImagesFinder* finder =
driver->server_context()->critical_images_finder();
if (finder->Available(driver) == CriticalImagesFinder::kNoDataYet) {
// Don't lazyload images on a page that's waiting for critical image data.
// However, this page should later be rewritten when data arrives. Contrast
// this with the case where beaconing is explicitly disabled, and all images
// are lazy loaded.
return RewriterHtmlApplication::DISABLED;
}
return RewriterHtmlApplication::ACTIVE;
}
void LazyloadImagesFilter::StartElementImpl(HtmlElement* element) {
if (noscript_element() != NULL) {
return;
}
if (!main_script_inserted_ && head_element_ == NULL) {
switch (element->keyword()) {
case HtmlName::kHtml:
case HtmlName::kLink:
case HtmlName::kMeta:
case HtmlName::kScript:
case HtmlName::kStyle:
break;
case HtmlName::kHead:
head_element_ = element;
break;
default:
InsertLazyloadJsCode(element);
break;
}
}
if (skip_rewrite_ == NULL) {
if (element->keyword() == HtmlName::kNoembed ||
element->keyword() == HtmlName::kMarquee) {
skip_rewrite_ = element;
return;
}
// Check if lazyloading is enabled for the given class name. If not,
// skip rewriting all images till we reach the end of this element.
HtmlElement::Attribute* class_attribute = element->FindAttribute(
HtmlName::kClass);
if (class_attribute != NULL) {
StringPiece class_value(class_attribute->DecodedValueOrNull());
if (!class_value.empty()) {
GoogleString class_string;
class_value.CopyToString(&class_string);
LowerString(&class_string);
if (!driver()->options()->IsLazyloadEnabledForClassName(
class_string)) {
skip_rewrite_ = element;
return;
}
}
}
}
if (element->keyword() == HtmlName::kScript) {
// This filter does not currently work with the jquery slider. We just don't
// rewrite the page in this case.
HtmlElement::Attribute* src = element->FindAttribute(HtmlName::kSrc);
if (src != NULL) {
StringPiece url(src->DecodedValueOrNull());
if (url.find(kJquerySlider) != StringPiece::npos) {
abort_rewrite_ = true;
return;
}
}
InsertOverrideAttributesScript(element, true);
}
}
void LazyloadImagesFilter::EndElementImpl(HtmlElement* element) {
if (noscript_element() != NULL || skip_rewrite_ != NULL) {
if (skip_rewrite_ == element) {
skip_rewrite_ = NULL;
}
return;
}
if (head_element_ == element) {
InsertLazyloadJsCode(NULL);
head_element_ = NULL;
}
if (abort_rewrite_) {
if (!abort_script_inserted_ && main_script_inserted_ &&
num_images_lazily_loaded_ > 0) {
// If we have already rewritten some elements on the page, insert a
// script to load all previously rewritten images.
HtmlElement* script = driver()->NewElement(element, HtmlName::kScript);
driver()->AddAttribute(script, HtmlName::kType, "text/javascript");
HtmlNode* script_code = driver()->NewCharactersNode(
script, kLoadAllImages);
driver()->InsertNodeAfterNode(element, script);
driver()->AppendChild(script, script_code);
abort_script_inserted_ = true;
}
return;
}
if (element->keyword() == HtmlName::kBody) {
InsertOverrideAttributesScript(element, false);
return;
}
// Only rewrite <img> tags. Don't rewrite <input> tags since the onload
// event is not fired for them in some browsers.
if (!driver()->IsRewritable(element) ||
element->keyword() != HtmlName::kImg) {
return;
}
HtmlElement::Attribute* src = element->FindAttribute(HtmlName::kSrc);
if (src == NULL) {
return;
}
StringPiece url(src->DecodedValueOrNull());
if (url.empty() || IsDataUrl(url) ||
element->FindAttribute(HtmlName::kDataPagespeedNoDefer) != NULL ||
element->FindAttribute(HtmlName::kPagespeedNoDefer) != NULL) {
// TODO(rahulbansal): Log separately for pagespeed_no_defer.
return;
}
AbstractLogRecord* log_record = driver()->log_record();
if (!CanAddPagespeedOnloadToImage(*element) ||
element->FindAttribute(HtmlName::kDataPagespeedLazySrc) != NULL ||
element->FindAttribute(HtmlName::kDataSrc) != NULL) {
log_record->LogLazyloadFilter(
RewriteOptions::FilterId(RewriteOptions::kLazyloadImages),
RewriterApplication::NOT_APPLIED, false, false);
return;
}
// Decode the url if it is rewritten.
GoogleUrl gurl(base_url(), url);
StringVector decoded_url_vector;
if (driver()->DecodeUrl(gurl, &decoded_url_vector) &&
decoded_url_vector.size() == 1) {
// We only handle the case where the rewritten url corresponds to
// a single original url which should be sufficient for all cases
// other than image sprites.
gurl.Reset(decoded_url_vector[0]);
}
if (!gurl.IsAnyValid()) {
// Do not lazily load images with invalid urls.
return;
}
StringPiece full_url = gurl.Spec();
if (full_url.empty()) {
return;
}
if (!driver()->options()->IsAllowed(full_url)) {
// Do not lazily load images with blacklisted urls.
log_record->LogLazyloadFilter(
RewriteOptions::FilterId(RewriteOptions::kLazyloadImages),
RewriterApplication::NOT_APPLIED, true, false);
return;
}
CriticalImagesFinder* finder =
driver()->server_context()->critical_images_finder();
// Note that if the platform lacks a CriticalImageFinder implementation, we
// consider all images to be non-critical and try to lazily load them.
// Similarly, if we have disabled data gathering for lazy load, we again lazy
// load all images. If, however, we simply haven't gathered enough data yet,
// we consider all images to be critical and disable lazy loading (in
// ShouldApply above) in order to provide better above-the-fold loading.
if (finder->Available(driver()) == CriticalImagesFinder::kAvailable) {
// Decode the url since the critical images in the finder are not
// rewritten.
if (finder->IsHtmlCriticalImage(full_url, driver())) {
log_record->LogLazyloadFilter(
RewriteOptions::FilterId(RewriteOptions::kLazyloadImages),
RewriterApplication::NOT_APPLIED, false, true);
// Do not try to lazily load this image since it is critical.
return;
}
}
if (!main_script_inserted_) {
InsertLazyloadJsCode(element);
}
// Replace the src with data-pagespeed-lazy-src.
driver()->SetAttributeName(src, HtmlName::kDataPagespeedLazySrc);
// Rename srcset -> data-pagespeed-high-res-srcset
HtmlElement::Attribute* srcset =
element->FindAttribute(HtmlName::kSrcset);
if (srcset != NULL) {
driver()->SetAttributeName(srcset, HtmlName::kDataPagespeedLazySrcset);
}
driver()->AddAttribute(element, HtmlName::kSrc, blank_image_url_);
log_record->LogLazyloadFilter(
RewriteOptions::FilterId(RewriteOptions::kLazyloadImages),
RewriterApplication::APPLIED_OK, false, false);
// Add an onload function to load the image if it is visible and then do
// the criticality check. Since we check CanAddPagespeedOnloadToImage
// before coming here, the only onload handler that we would delete would
// be the one added by our very own beaconing code. We re-introduce this
// beaconing onload logic via kImageOnloadCode.
// TODO(jud): Add these with addEventListener rather than with the attributes.
element->DeleteAttribute(HtmlName::kOnload);
driver()->AddAttribute(element, HtmlName::kOnload, kImageOnloadCode);
// Add onerror handler just in case the temporary pixel doesn't load.
element->DeleteAttribute(HtmlName::kOnerror);
// Note: this.onerror=null to avoid infinitely repeating on failure:
// See: http://stackoverflow.com/questions/3984287
driver()->AddAttribute(element, HtmlName::kOnerror,
StrCat("this.onerror=null;", kImageOnloadCode));
++num_images_lazily_loaded_;
}
void LazyloadImagesFilter::InsertLazyloadJsCode(HtmlElement* element) {
if (!driver()->is_lazyload_script_flushed() &&
(!abort_rewrite_ || num_images_lazily_loaded_ > 0)) {
HtmlElement* script = driver()->NewElement(element, HtmlName::kScript);
if (element != NULL) {
driver()->InsertNodeBeforeNode(element, script);
} else if (driver()->CanAppendChild(head_element_)) {
// insert at end of head.
driver()->AppendChild(head_element_, script);
} else {
// Could not insert at end of head even though we just saw the end of head
// event! Should not happen, but this will ensure that we insert the
// script before the next tag we see.
LOG(DFATAL) << "Can't append child to <head> at the </head> event!";
main_script_inserted_ = false;
return;
}
StaticAssetManager* static_asset_manager =
driver()->server_context()->static_asset_manager();
GoogleString lazyload_js = GetLazyloadJsSnippet(
driver()->options(), static_asset_manager);
AddJsToElement(lazyload_js, script);
driver()->AddAttribute(script, HtmlName::kDataPagespeedNoDefer, NULL);
}
main_script_inserted_ = true;
}
void LazyloadImagesFilter::InsertOverrideAttributesScript(
HtmlElement* element, bool is_before_script) {
if (num_images_lazily_loaded_ > 0) {
HtmlElement* script = driver()->NewElement(element, HtmlName::kScript);
driver()->AddAttribute(script, HtmlName::kType, "text/javascript");
driver()->AddAttribute(script, HtmlName::kDataPagespeedNoDefer, NULL);
HtmlNode* script_code = driver()->NewCharactersNode(
script, kOverrideAttributeFunctions);
if (is_before_script) {
driver()->InsertNodeBeforeNode(element, script);
} else {
driver()->AppendChild(element, script);
}
driver()->AppendChild(script, script_code);
num_images_lazily_loaded_ = 0;
}
}
GoogleString LazyloadImagesFilter::GetBlankImageSrc(
const RewriteOptions* options,
const StaticAssetManager* static_asset_manager) {
const GoogleString& options_url = options->lazyload_images_blank_url();
if (options_url.empty()) {
return static_asset_manager->GetAssetUrl(StaticAssetEnum::BLANK_GIF,
options);
} else {
return options_url;
}
}
GoogleString LazyloadImagesFilter::GetLazyloadJsSnippet(
const RewriteOptions* options,
StaticAssetManager* static_asset_manager) {
const GoogleString& load_onload =
options->lazyload_images_after_onload() ? kTrue : kFalse;
StringPiece lazyload_images_js =
static_asset_manager->GetAsset(
StaticAssetEnum::LAZYLOAD_IMAGES_JS, options);
const GoogleString& blank_image_url =
GetBlankImageSrc(options, static_asset_manager);
GoogleString lazyload_js =
StrCat(lazyload_images_js, "\npagespeed.lazyLoadInit(",
load_onload, ", \"", blank_image_url, "\");\n");
return lazyload_js;
}
} // namespace net_instaweb