blob: 5723280c87a523d136749ed2cdf2154889a95247 [file] [log] [blame]
/*
* Copyright 2013 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: jmaessen@google.com (Jan-Willem Maessen)
#include "net/instaweb/rewriter/public/critical_css_beacon_filter.h"
#include <set>
#include <vector>
#include "net/instaweb/rewriter/public/critical_finder_support_util.h"
#include "net/instaweb/rewriter/public/critical_selector_finder.h"
#include "net/instaweb/rewriter/public/css_tag_scanner.h"
#include "net/instaweb/rewriter/public/css_util.h"
#include "net/instaweb/rewriter/public/request_properties.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_driver_factory.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 "pagespeed/kernel/base/escaping.h"
#include "pagespeed/kernel/base/hasher.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/html/html_element.h"
#include "pagespeed/kernel/html/html_name.h"
#include "pagespeed/kernel/http/google_url.h"
#include "webutil/css/media.h"
#include "webutil/css/parser.h"
#include "webutil/css/selector.h"
using Css::Selectors;
using Css::Stylesheet;
using Css::Ruleset;
using Css::Rulesets;
namespace net_instaweb {
const char CriticalCssBeaconFilter::kInitializePageSpeedJs[] =
"var pagespeed = pagespeed || {};";
// Counters.
const char CriticalCssBeaconFilter::kCriticalCssBeaconAddedCount[] =
"critical_css_beacon_filter_script_added_count";
const char CriticalCssBeaconFilter::kCriticalCssNoBeaconDueToMissingData[] =
"critical_css_no_beacon_due_to_missing_data";
const char CriticalCssBeaconFilter::kCriticalCssSkippedDueToCharset[] =
"critical_css_skipped_due_to_charset";
CriticalCssBeaconFilter::CriticalCssBeaconFilter(RewriteDriver* driver)
: CssSummarizerBase(driver) {
Statistics* stats = driver->server_context()->statistics();
critical_css_beacon_added_count_ = stats->GetVariable(
kCriticalCssBeaconAddedCount);
critical_css_no_beacon_due_to_missing_data_ = stats->GetVariable(
kCriticalCssNoBeaconDueToMissingData);
critical_css_skipped_due_to_charset_ = stats->GetVariable(
kCriticalCssSkippedDueToCharset);
}
CriticalCssBeaconFilter::~CriticalCssBeaconFilter() {}
void CriticalCssBeaconFilter::InitStats(Statistics* statistics) {
statistics->AddVariable(kCriticalCssBeaconAddedCount);
statistics->AddVariable(kCriticalCssNoBeaconDueToMissingData);
statistics->AddVariable(kCriticalCssSkippedDueToCharset);
}
bool CriticalCssBeaconFilter::MustSummarize(HtmlElement* element) const {
// Don't summarize alternate stylesheets, they are clearly non-critical.
if (element->keyword() == HtmlName::kLink &&
CssTagScanner::IsAlternateStylesheet(
element->AttributeValue(HtmlName::kRel))) {
return false;
}
// Don't summarize non-screen-affecting or <noscript> CSS at all; the time we
// spend doing that is better devoted to summarizing CSS selectors we will
// actually consider critical.
return (noscript_element() == NULL) &&
css_util::CanMediaAffectScreen(element->AttributeValue(HtmlName::kMedia));
}
void CriticalCssBeaconFilter::Summarize(Stylesheet* stylesheet,
GoogleString* out) const {
StringSet selectors;
FindSelectorsFromStylesheet(*stylesheet, &selectors);
// Serialize set into out.
AppendJoinCollection(out, selectors, ",");
}
// Append the selector list initialization JavaScript to |script|.
// Right now the result looks like:
// pagespeed.selectors=["selector 1","selector 2","selector 3"];
void CriticalCssBeaconFilter::AppendSelectorsInitJs(
GoogleString* script, const StringSet& selectors) {
StrAppend(script, "pagespeed.selectors=[");
for (StringSet::const_iterator i = selectors.begin();
i != selectors.end(); ++i) {
if (i != selectors.begin()) {
StrAppend(script, ",");
}
EscapeToJsStringLiteral(*i, true /* quote */, script);
}
StrAppend(script, "];");
}
// Append the beacon initialization JavaScript to |script|.
// Right now the result looks like:
// pagespeed.criticalCssBeaconInit('beacon_url','page_url','options_hash',
// pagespeed.selectors);
void CriticalCssBeaconFilter::AppendBeaconInitJs(
const BeaconMetadata& metadata, GoogleString* script) {
GoogleString beacon_url = driver()->IsHttps() ?
driver()->options()->beacon_url().https :
driver()->options()->beacon_url().http;
GoogleString page_url;
EscapeToJsStringLiteral(driver()->google_url().Spec(), false /* add_quotes */,
&page_url);
Hasher* hasher = driver()->server_context()->hasher();
GoogleString options_hash = hasher->Hash(driver()->options()->signature());
StrAppend(script,
"pagespeed.criticalCssBeaconInit('",
beacon_url, "','", page_url, "','",
options_hash, "','", metadata.nonce, "',pagespeed.selectors);");
}
void CriticalCssBeaconFilter::SummariesDone() {
// We parse each summary back into component selectors from its
// comma-separated string, using a StringSet to remove duplicates (they'll be
// sorted, too, which makes this easier to test). We re-serialize the set.
StringSet selectors;
for (int i = 0; i < NumStyles(); ++i) {
const SummaryInfo& summary_info = GetSummaryForStyle(i);
// The critical_selector_filter doesn't include <noscript>-specific CSS
// in the critical CSS it computes; so there is no need to figure out
// critical selectors for such CSS.
if (summary_info.is_inside_noscript) {
continue;
}
switch (summary_info.state) {
case kSummaryStillPending:
// Don't beacon if we're still waiting for critical selector data.
return;
case kSummaryOk: {
// Include the selectors in the beacon
StringPieceVector temp;
SplitStringPieceToVector(summary_info.data, ",", &temp,
true /* omit_empty_strings */);
for (StringPieceVector::const_iterator i = temp.begin(),
end = temp.end();
i != end; ++i) {
selectors.insert(i->as_string());
}
break;
}
case kSummarySlotRemoved:
// Another filter (likely combine CSS) has eliminated this CSS.
continue;
case kSummaryCssParseError:
case kSummaryResourceCreationFailed:
case kSummaryInputUnavailable:
// The CSS couldn't be fetched or parsed in some fashion. This will
// be left in place by the rewriter, so we don't need to consider it for
// beaconing either. NOTE: this requires the rewriter to inject
// critical CSS in situ so that we don't disrupt the cascade order
// around the unparseable data.
// TODO(jmaessen): Consider handling unparseable data within the CSS
// parse tree, which would let us extract critical CSS selectors from
// CSS with a mix of parseable and unparseable rules.
continue;
}
}
BeaconMetadata metadata =
driver()->server_context()->critical_selector_finder()->
PrepareForBeaconInsertion(selectors, driver());
if (metadata.status == kDoNotBeacon) {
// No beaconing required according to current pcache state and computed
// selector set.
return;
}
// Insert the beaconing code and selectors.
GoogleString script;
StaticAssetManager* asset_manager =
driver()->server_context()->static_asset_manager();
if (driver()->server_context()->factory()->UseBeaconResultsInFilters()) {
script = asset_manager->GetAsset(
StaticAssetEnum::CRITICAL_CSS_BEACON_JS, driver()->options());
AppendSelectorsInitJs(&script, selectors);
AppendBeaconInitJs(metadata, &script);
} else {
script = kInitializePageSpeedJs;
AppendSelectorsInitJs(&script, selectors);
}
HtmlElement* script_element = driver()->NewElement(NULL, HtmlName::kScript);
driver()->AddAttribute(script_element, HtmlName::kDataPagespeedNoDefer, NULL);
InsertNodeAtBodyEnd(script_element);
AddJsToElement(script, script_element);
if (critical_css_beacon_added_count_ != NULL) {
critical_css_beacon_added_count_->Add(1);
}
}
void CriticalCssBeaconFilter::DetermineEnabled(GoogleString* disabled_reason) {
set_is_enabled(driver()->request_properties()->SupportsCriticalCssBeacon());
}
void CriticalCssBeaconFilter::FindSelectorsFromRuleset(
const Ruleset& ruleset, StringSet* selectors) {
const Selectors& rule_selectors = ruleset.selectors();
for (int i = 0, n = rule_selectors.size(); i < n; ++i) {
GoogleString trimmed(css_util::JsDetectableSelector(*rule_selectors[i]));
if (!trimmed.empty()) {
// Non-empty trimmed selector. An empty trimmed selector (eg :hover,
// which gets stripped away as it's not JS detectable) is *automatically*
// critical, and we could also ignore the selector * (:hover is implicitly
// *:hover).
selectors->insert(trimmed);
}
}
}
// Returns false on parse failure, else records css selectors (in normalized
// string form) in selectors. The selectors will be sorted and unique. Logging
// of failures etc. should be done in the caller.
void CriticalCssBeaconFilter::FindSelectorsFromStylesheet(
const Stylesheet& css, StringSet* selectors) {
const Rulesets& rulesets = css.rulesets();
for (int i = 0, n = rulesets.size(); i < n; ++i) {
Ruleset* ruleset = rulesets[i];
if (ruleset->type() == Ruleset::UNPARSED_REGION) {
// Couldn't parse this as a rule.
continue;
}
// Skip rules that can't apply to the screen.
if (!css_util::CanMediaAffectScreen(ruleset->media_queries().ToString())) {
continue;
}
// Record the selectors associated with this ruleset.
FindSelectorsFromRuleset(*ruleset, selectors);
}
}
} // namespace net_instaweb