Bug 1400353 - Add faster loading, favicon positioning and bug fixes to Activity Stream. r?k88hudson draft
authorEd Lee <edilee@mozilla.com>
Fri, 15 Sep 2017 16:58:32 -0700
changeset 665811 29832bc58ada8a482b0b060d57d74faf73b817f0
parent 665648 6be5c7d30d2def62a762ac187252eba626b23a92
child 731896 18e425941c9185eba655e49a55eb47e7fcef5e33
push id80188
push userbmo:edilee@mozilla.com
push dateSat, 16 Sep 2017 00:01:23 +0000
reviewersk88hudson
bugs1400353
milestone57.0a1
Bug 1400353 - Add faster loading, favicon positioning and bug fixes to Activity Stream. r?k88hudson MozReview-Commit-ID: K47CeF7IJtB
browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
browser/extensions/activity-stream/data/content/activity-stream.bundle.js
browser/extensions/activity-stream/data/content/activity-stream.css
browser/extensions/activity-stream/data/content/activity-stream.html
browser/extensions/activity-stream/data/locales.json
browser/extensions/activity-stream/install.rdf.in
browser/extensions/activity-stream/lib/ActivityStream.jsm
browser/extensions/activity-stream/lib/HighlightsFeed.jsm
browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
--- a/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
@@ -8,18 +8,30 @@
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"><div class="outer-wrapper fixed-to-top" data-reactroot="" data-reactid="1" data-react-checksum="544412221"><main data-reactid="2"><div class="search-wrapper" data-reactid="3"><label for="newtab-search-text" class="search-label" data-reactid="4"><span class="sr-only" data-reactid="5"><span data-reactid="6">Search the Web</span></span></label><input type="search" id="newtab-search-text" maxlength="256" placeholder="Search the Web" title="Search the Web" data-reactid="7"/><button id="searchSubmit" class="search-button" title=" " data-reactid="8"><span class="sr-only" data-reactid="9"><span data-reactid="10"> </span></span></button></div><section class="top-sites" data-reactid="11"><h3 class="section-title" data-reactid="12"><span class="icon icon-small-spacer icon-topsites" data-reactid="13"></span><span data-reactid="14"> </span></h3><ul class="top-sites-list" data-reactid="15"><li class="top-site-outer placeholder" data-reactid="16"><a data-reactid="17"><div class="tile" aria-hidden="true" data-reactid="18"><span class="letter-fallback" data-reactid="19"></span><div class="screenshot" style="background-image:none;" data-reactid="20"></div></div><div class="title " data-reactid="21"><span dir="auto" data-reactid="22"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="23"><a data-reactid="24"><div class="tile" aria-hidden="true" data-reactid="25"><span class="letter-fallback" data-reactid="26"></span><div class="screenshot" style="background-image:none;" data-reactid="27"></div></div><div class="title " data-reactid="28"><span dir="auto" data-reactid="29"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="30"><a data-reactid="31"><div class="tile" aria-hidden="true" data-reactid="32"><span class="letter-fallback" data-reactid="33"></span><div class="screenshot" style="background-image:none;" data-reactid="34"></div></div><div class="title " data-reactid="35"><span dir="auto" data-reactid="36"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="37"><a data-reactid="38"><div class="tile" aria-hidden="true" data-reactid="39"><span class="letter-fallback" data-reactid="40"></span><div class="screenshot" style="background-image:none;" data-reactid="41"></div></div><div class="title " data-reactid="42"><span dir="auto" data-reactid="43"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="44"><a data-reactid="45"><div class="tile" aria-hidden="true" data-reactid="46"><span class="letter-fallback" data-reactid="47"></span><div class="screenshot" style="background-image:none;" data-reactid="48"></div></div><div class="title " data-reactid="49"><span dir="auto" data-reactid="50"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="51"><a data-reactid="52"><div class="tile" aria-hidden="true" data-reactid="53"><span class="letter-fallback" data-reactid="54"></span><div class="screenshot" style="background-image:none;" data-reactid="55"></div></div><div class="title " data-reactid="56"><span dir="auto" data-reactid="57"></span></div></a></li></ul><div class="edit-topsites-wrapper" data-reactid="58"><div class="edit-topsites-button" data-reactid="59"><button class="edit" title=" " data-reactid="60"><span data-reactid="61"> </span></button></div></div></section><div class="sections-list" data-reactid="62"><section data-reactid="63"><div class="section-top-bar" data-reactid="64"><h3 class="section-title" data-reactid="65"><span class="icon icon-small-spacer icon-pocket" data-reactid="66"></span><span data-reactid="67"> </span></h3></div><ul class="section-list" style="padding:0;" data-reactid="68"><li class="card-outer placeholder" data-reactid="69"><a data-reactid="70"><div class="card" data-reactid="71"><div class="card-details no-image" data-reactid="72"><div class="card-text no-context no-description no-host-name no-image" data-reactid="73"><h4 class="card-title" dir="auto" data-reactid="74"></h4><p class="card-description" dir="auto" data-reactid="75"></p></div><div class="card-context" data-reactid="76"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="77"><a data-reactid="78"><div class="card" data-reactid="79"><div class="card-details no-image" data-reactid="80"><div class="card-text no-context no-description no-host-name no-image" data-reactid="81"><h4 class="card-title" dir="auto" data-reactid="82"></h4><p class="card-description" dir="auto" data-reactid="83"></p></div><div class="card-context" data-reactid="84"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="85"><a data-reactid="86"><div class="card" data-reactid="87"><div class="card-details no-image" data-reactid="88"><div class="card-text no-context no-description no-host-name no-image" data-reactid="89"><h4 class="card-title" dir="auto" data-reactid="90"></h4><p class="card-description" dir="auto" data-reactid="91"></p></div><div class="card-context" data-reactid="92"></div></div></div></a></li></ul><div class="topic" data-reactid="93"><span data-reactid="94"><span data-reactid="95"> </span></span><ul data-reactid="96"><li data-reactid="97"><a class="topic-link" data-reactid="98"></a></li></ul></div></section><section data-reactid="99"><div class="section-top-bar" data-reactid="100"><h3 class="section-title" data-reactid="101"><span class="icon icon-small-spacer icon-highlights" data-reactid="102"></span><span data-reactid="103"> </span></h3></div><ul class="section-list" style="padding:0;" data-reactid="104"><li class="card-outer placeholder" data-reactid="105"><a data-reactid="106"><div class="card" data-reactid="107"><div class="card-details no-image" data-reactid="108"><div class="card-text no-context no-description no-host-name no-image" data-reactid="109"><h4 class="card-title" dir="auto" data-reactid="110"></h4><p class="card-description" dir="auto" data-reactid="111"></p></div><div class="card-context" data-reactid="112"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="113"><a data-reactid="114"><div class="card" data-reactid="115"><div class="card-details no-image" data-reactid="116"><div class="card-text no-context no-description no-host-name no-image" data-reactid="117"><h4 class="card-title" dir="auto" data-reactid="118"></h4><p class="card-description" dir="auto" data-reactid="119"></p></div><div class="card-context" data-reactid="120"></div></div></div></a></li><li class="card-outer placeholder" data-reactid="121"><a data-reactid="122"><div class="card" data-reactid="123"><div class="card-details no-image" data-reactid="124"><div class="card-text no-context no-description no-host-name no-image" data-reactid="125"><h4 class="card-title" dir="auto" data-reactid="126"></h4><p class="card-description" dir="auto" data-reactid="127"></p></div><div class="card-context" data-reactid="128"></div></div></div></a></li></ul></section></div><!-- react-empty: 129 --></main></div></div>
     <div id="snippets-container">
       <div id="snippets"></div>
     </div>
-<script src="resource://activity-stream/data/content/activity-stream-initial-state.js"></script>
-    <script src="chrome://browser/content/contentSearchUI.js"></script>
-    <script src="resource://activity-stream/vendor/react.js"></script>
-    <script src="resource://activity-stream/vendor/react-dom.js"></script>
-    <script src="resource://activity-stream/vendor/react-intl.js"></script>
-    <script src="resource://activity-stream/vendor/redux.js"></script>
-    <script src="resource://activity-stream/vendor/react-redux.js"></script>
-    <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
+    <script>
+// Don't directly load the following scripts as part of html to let the page
+// finish loading to render the content sooner.
+for (const src of [
+  "resource://activity-stream/data/content/activity-stream-initial-state.js",
+  "chrome://browser/content/contentSearchUI.js",
+  "resource://activity-stream/vendor/react.js",
+  "resource://activity-stream/vendor/react-dom.js",
+  "resource://activity-stream/vendor/react-intl.js",
+  "resource://activity-stream/vendor/redux.js",
+  "resource://activity-stream/vendor/react-redux.js",
+  "resource://activity-stream/data/content/activity-stream.bundle.js"
+]) {
+  // These dynamically inserted scripts by default are async, but we need them
+  // to load in the desired order (i.e., bundle last).
+  const script = document.body.appendChild(document.createElement("script"));
+  script.async = false;
+  script.src = src;
+}
+    </script>
   </body>
 </html>
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -2254,17 +2254,17 @@ const getFormattedMessage = message => t
   "span",
   null,
   message
 ) : React.createElement(FormattedMessage, message);
 
 const PreferencesInput = props => React.createElement(
   "section",
   null,
-  React.createElement("input", { type: "checkbox", id: props.prefName, name: props.prefName, checked: props.value, onChange: props.onChange, className: props.className }),
+  React.createElement("input", { type: "checkbox", id: props.prefName, name: props.prefName, checked: props.value, disabled: props.disabled, onChange: props.onChange, className: props.className }),
   React.createElement(
     "label",
     { htmlFor: props.prefName, className: props.labelClassName },
     getFormattedMessage(props.titleString)
   ),
   props.descString && React.createElement(
     "p",
     { className: "prefs-input-description" },
@@ -2354,18 +2354,19 @@ class PreferencesPane extends React.Pure
             ),
             React.createElement(PreferencesInput, { className: "showSearch", prefName: "showSearch", value: prefs.showSearch, onChange: this.handlePrefChange,
               titleString: { id: "settings_pane_search_header" }, descString: { id: "settings_pane_search_body" } }),
             React.createElement("hr", null),
             React.createElement(PreferencesInput, { className: "showTopSites", prefName: "showTopSites", value: prefs.showTopSites, onChange: this.handlePrefChange,
               titleString: { id: "settings_pane_topsites_header" }, descString: { id: "settings_pane_topsites_body" } }),
             React.createElement(
               "div",
-              { className: "options" },
-              React.createElement(PreferencesInput, { className: "showMoreTopSites", prefName: "topSitesCount", value: prefs.topSitesCount !== TOP_SITES_DEFAULT_LENGTH, onChange: this.handlePrefChange,
+              { className: `options${prefs.showTopSites ? "" : " disabled"}` },
+              React.createElement(PreferencesInput, { className: "showMoreTopSites", prefName: "topSitesCount", disabled: !prefs.showTopSites,
+                value: prefs.topSitesCount !== TOP_SITES_DEFAULT_LENGTH, onChange: this.handlePrefChange,
                 titleString: { id: "settings_pane_topsites_options_showmore" }, labelClassName: "icon icon-topsites" })
             ),
             sections.filter(section => !section.shouldHidePref).map(({ id, title, enabled, pref }) => React.createElement(PreferencesInput, { key: id, className: "showSection", prefName: pref && pref.feed || id,
               value: enabled, onChange: pref && pref.feed ? this.handlePrefChange : this.handleSectionChange,
               titleString: pref && pref.titleString || title, descString: pref && pref.descString })),
             React.createElement("hr", null),
             React.createElement(PreferencesInput, { className: "showSnippets", prefName: "feeds.snippets",
               value: prefs["feeds.snippets"], onChange: this.handlePrefChange,
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -331,19 +331,19 @@ main {
     .top-sites-list .top-site-outer .rich-icon {
       top: 0;
       offset-inline-start: 0;
       height: 100%;
       width: 100%;
       background-size: 96px; }
     .top-sites-list .top-site-outer .default-icon {
       z-index: 1;
-      top: -6px;
-      offset-inline-start: -6px;
+      bottom: -6px;
       height: 42px;
+      offset-inline-end: -6px;
       width: 42px;
       background-size: 32px;
       display: flex;
       align-items: center;
       justify-content: center;
       font-size: 20px; }
     .top-sites-list .top-site-outer .title {
       font: message-box;
@@ -807,16 +807,18 @@ main {
         line-height: 19px; }
     .prefs-pane .prefs-modal-inner-wrapper .options {
       background: #F9F9FA;
       border: 1px solid #D7D7DB;
       border-radius: 2px;
       margin: -10px 0 20px;
       margin-inline-start: 30px;
       padding: 10px; }
+      .prefs-pane .prefs-modal-inner-wrapper .options.disabled {
+        opacity: 0.5; }
       .prefs-pane .prefs-modal-inner-wrapper .options label {
         background-position-x: 35px;
         background-position-y: 2.5px;
         background-repeat: no-repeat;
         display: inline-block;
         font-size: 14px;
         font-weight: normal;
         height: auto;
@@ -838,18 +840,18 @@ main {
     position: fixed;
     width: 400px; }
     .prefs-pane .actions button {
       margin-inline-end: 20px; }
   .prefs-pane [type='checkbox']:not(:checked),
   .prefs-pane [type='checkbox']:checked {
     offset-inline-start: -9999px;
     position: absolute; }
-  .prefs-pane [type='checkbox']:not(:checked) + label,
-  .prefs-pane [type='checkbox']:checked + label {
+  .prefs-pane [type='checkbox']:not(:disabled):not(:checked) + label,
+  .prefs-pane [type='checkbox']:not(:disabled):checked + label {
     cursor: pointer;
     padding: 0 30px;
     position: relative; }
   .prefs-pane [type='checkbox']:not(:checked) + label::before,
   .prefs-pane [type='checkbox']:checked + label::before {
     background: #FFF;
     border: 1px solid #B1B1B3;
     border-radius: 3px;
@@ -870,20 +872,20 @@ main {
     width: 21px;
     -moz-context-properties: fill, stroke;
     fill: #0060DF;
     stroke: none; }
   .prefs-pane [type='checkbox']:not(:checked) + label::after {
     opacity: 0; }
   .prefs-pane [type='checkbox']:checked + label::after {
     opacity: 1; }
-  .prefs-pane [type='checkbox'] + label:hover::before {
+  .prefs-pane [type='checkbox']:not(:disabled) + label:hover::before {
     border: 1px solid #0060DF; }
-  .prefs-pane [type='checkbox']:checked:focus + label::before,
-  .prefs-pane [type='checkbox']:not(:checked):focus + label::before {
+  .prefs-pane [type='checkbox']:not(:disabled):checked:focus + label::before,
+  .prefs-pane [type='checkbox']:not(:disabled):not(:checked):focus + label::before {
     border: 1px dotted #0060DF; }
 
 .prefs-pane-button button {
   background-color: transparent;
   border: 0;
   cursor: pointer;
   fill: rgba(12, 12, 13, 0.6);
   padding: 15px;
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -8,17 +8,29 @@
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"></div>
     <div id="snippets-container">
       <div id="snippets"></div>
     </div>
-    <script src="chrome://browser/content/contentSearchUI.js"></script>
-    <script src="resource://activity-stream/vendor/react.js"></script>
-    <script src="resource://activity-stream/vendor/react-dom.js"></script>
-    <script src="resource://activity-stream/vendor/react-intl.js"></script>
-    <script src="resource://activity-stream/vendor/redux.js"></script>
-    <script src="resource://activity-stream/vendor/react-redux.js"></script>
-    <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
+    <script>
+// Don't directly load the following scripts as part of html to let the page
+// finish loading to render the content sooner.
+for (const src of [
+  "chrome://browser/content/contentSearchUI.js",
+  "resource://activity-stream/vendor/react.js",
+  "resource://activity-stream/vendor/react-dom.js",
+  "resource://activity-stream/vendor/react-intl.js",
+  "resource://activity-stream/vendor/redux.js",
+  "resource://activity-stream/vendor/react-redux.js",
+  "resource://activity-stream/data/content/activity-stream.bundle.js"
+]) {
+  // These dynamically inserted scripts by default are async, but we need them
+  // to load in the desired order (i.e., bundle last).
+  const script = document.body.appendChild(document.createElement("script"));
+  script.async = false;
+  script.src = src;
+}
+    </script>
   </body>
 </html>
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -6058,33 +6058,33 @@
     "menu_action_pin": "Sabitle",
     "menu_action_unpin": "Sabitleneni kaldır",
     "confirm_history_delete_p1": "Bu sayfanın tüm kayıtlarını geçmişinizden silmek istediğinizden emin misiniz?",
     "confirm_history_delete_notice_p2": "Bu işlem geri alınamaz.",
     "menu_action_save_to_pocket": "Pocket’a kaydet",
     "search_for_something_with": "{search_term} terimini şununla ara:",
     "search_button": "Ara",
     "search_header": "{search_engine_name} Araması",
-    "search_web_placeholder": "Web'de ara",
+    "search_web_placeholder": "Web’de ara",
     "search_settings": "Arama ayarlarını değiştir",
     "section_info_option": "Bilgi",
     "section_info_send_feedback": "Görüş gönder",
     "section_info_privacy_notice": "Gizlilik bildirimi",
     "welcome_title": "Yeni sekmeye hoş geldiniz",
     "welcome_body": "Firefox son zamanlarda ziyaret ettiğiniz ve sık kullandığınız yer imlerini, makaleleri, videoları ve sayfaları onlara tekrar kolayca geri dönebilmeniz için bu alanda gösterecektir.",
     "welcome_label": "Öne Çıkanlar'ınızı tanıyın",
     "time_label_less_than_minute": "<1 dk",
     "time_label_minute": "{number} dk",
     "time_label_hour": "{number} sa",
     "time_label_day": "{number} g",
     "settings_pane_button_label": "Yeni Sekme sayfanızı özelleştirin",
     "settings_pane_header": "Yeni Sekme Tercihleri",
     "settings_pane_body2": "Bu sayfada görmek istediklerinizi seçin.",
     "settings_pane_search_header": "Arama",
-    "settings_pane_search_body": "Yeni sekme üzerinden web'de arama yapın.",
+    "settings_pane_search_body": "Yeni sekme üzerinden web’de arama yapın.",
     "settings_pane_topsites_header": "Sık Kullandıklarınız",
     "settings_pane_topsites_body": "En sık ziyaret ettiğiniz web sitelerine erişin.",
     "settings_pane_topsites_options_showmore": "İki satır göster",
     "settings_pane_bookmarks_header": "Son Yer İmleri",
     "settings_pane_bookmarks_body": "Yeni eklediğiniz yer imlerini bir araya topladık.",
     "settings_pane_visit_again_header": "Yeniden Ziyaret Edin",
     "settings_pane_visit_again_body": "Firefox, gezinti geçmişinizden hatırlamak veya yeniden ziyaret etmek isteyebileceğiniz sayfaları burada gösterecek.",
     "settings_pane_highlights_header": "Öne çıkanlar",
--- a/browser/extensions/activity-stream/install.rdf.in
+++ b/browser/extensions/activity-stream/install.rdf.in
@@ -3,17 +3,17 @@
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>activity-stream@mozilla.org</em:id>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
-    <em:version>2017.09.14.1322-706b3303</em:version>
+    <em:version>2017.09.16.0001-2fc82208</em:version>
     <em:name>Activity Stream</em:name>
     <em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -47,22 +47,21 @@ const PREFS_CONFIG = new Map([
   }],
   ["feeds.section.topstories.options", {
     title: "Configuration options for top stories feed",
     // This is a dynamic pref as it depends on the feed being shown or not
     getValue: args => JSON.stringify({
       api_key_pref: "extensions.pocket.oAuthConsumerKey",
       // Use the opposite value as what default value the feed would have used
       hidden: !PREFS_CONFIG.get("feeds.section.topstories").getValue(args),
-      learn_more_endpoint: "https://getpocket.cdn.mozilla.net/firefox_learnmore?src=ff_newtab",
       provider_header: "pocket_feedback_header",
       provider_description: "pocket_description",
       provider_icon: "pocket",
       provider_name: "Pocket",
-      read_more_endpoint: "https://getpocket.cdn.mozilla.net/explore/trending?src=ff_new_tab",
+      read_more_endpoint: "https://getpocket.cdn.mozilla.net/explore/trending?src=fx_new_tab",
       stories_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=2&consumer_key=$apiKey&locale_lang=${args.locale}`,
       stories_referrer: "https://getpocket.com/recommendations",
       info_link: "https://www.mozilla.org/privacy/firefox/#pocketstories",
       topics_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/trending-topics?version=2&consumer_key=$apiKey&locale_lang=${args.locale}`,
       show_spocs: false,
       personalized: false
     })
   }],
--- a/browser/extensions/activity-stream/lib/HighlightsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/HighlightsFeed.jsm
@@ -25,17 +25,16 @@ const HIGHLIGHTS_UPDATE_TIME = 15 * 60 *
 const MANY_EXTRA_LENGTH = HIGHLIGHTS_MAX_LENGTH * 5 + TOP_SITES_SHOWMORE_LENGTH;
 const SECTION_ID = "highlights";
 
 this.HighlightsFeed = class HighlightsFeed {
   constructor() {
     this.highlightsLastUpdated = 0;
     this.highlights = [];
     this.dedupe = new Dedupe(this._dedupeKey);
-    this.imageCache = new Map();
   }
 
   _dedupeKey(site) {
     return site && site.url;
   }
 
   init() {
     SectionsManager.onceInitialized(this.postInit.bind(this));
@@ -69,30 +68,40 @@ this.HighlightsFeed = class HighlightsFe
 
     // Remove adult highlights if we need to
     const checkedAdult = this.store.getState().Prefs.values.filterAdult ?
       filterAdult(manyPages) : manyPages;
 
     // Remove any Highlights that are in Top Sites already
     const [, deduped] = this.dedupe.group(this.store.getState().TopSites.rows, checkedAdult);
 
+    // Store existing images in case we need to reuse them
+    const currentImages = {};
+    for (const site of this.highlights) {
+      if (site && site.image) {
+        currentImages[site.url] = site.image;
+      }
+    }
+
     // Keep all "bookmark"s and at most one (most recent) "history" per host
     this.highlights = [];
     const hosts = new Set();
     for (const page of deduped) {
       const hostname = shortURL(page);
       // Skip this history page if we already something from the same host
       if (page.type === "history" && hosts.has(hostname)) {
         continue;
       }
 
-      // If we already have the image for the card in the cache, use that
-      // immediately. Then asynchronously fetch the image (refreshes the cache).
-      const image = this.imageCache.get(page.url);
-      this.fetchImage(page.url, page.preview_image_url);
+      // If we already have the image for the card, use that immediately. Else
+      // asynchronously fetch the image.
+      const image = currentImages[page.url];
+      if (!image) {
+        this.fetchImage(page.url, page.preview_image_url);
+      }
 
       // We want the page, so update various fields for UI
       Object.assign(page, {
         image,
         hasImage: true, // We always have an image - fall back to a screenshot
         hostname,
         type: page.bookmarkGuid ? "bookmark" : page.type
       });
@@ -104,33 +113,32 @@ this.HighlightsFeed = class HighlightsFe
       // Skip the rest if we have enough items
       if (this.highlights.length === HIGHLIGHTS_MAX_LENGTH) {
         break;
       }
     }
 
     SectionsManager.updateSection(SECTION_ID, {rows: this.highlights}, this.highlightsLastUpdated === 0 || broadcast);
     this.highlightsLastUpdated = Date.now();
-    // Clearing the image cache here is okay. The asynchronous fetchImage calls
-    // get scheduled after the body of fetchHighlights has been executed, so they
-    // then fill up the cache again for the next fetchHighlights call.
-    this.imageCache.clear();
   }
 
   /**
-   * Fetch an image for a given highlight, store it in the image cache, and
-   * update the card with the new image. If the highlight has a preview image
-   * then use that, else fall back to a screenshot of the page.
+   * Fetch an image for a given highlight and update the card with it. If no
+   * image is available then fallback to fetching a screenshot. Update the card
+   * in `this.highlights` so that the image is cached for the next refresh.
    */
   async fetchImage(url, imageUrl) {
     const image = await Screenshots.getScreenshotForURL(imageUrl || url);
+    SectionsManager.updateSectionCard(SECTION_ID, url, {image}, true);
     if (image) {
-      this.imageCache.set(url, image);
+      const highlight = this.highlights.find(site => site.url === url);
+      if (highlight) {
+        highlight.image = image;
+      }
     }
-    SectionsManager.updateSectionCard(SECTION_ID, url, {image}, true);
   }
 
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
         break;
       case at.NEW_TAB_LOAD:
--- a/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
@@ -108,24 +108,27 @@ describe("Highlights Feed", () => {
       assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights);
     });
     it("should add hostname and hasImage to each link", async () => {
       links = [{url: "https://mozilla.org"}];
       await feed.fetchHighlights();
       assert.equal(feed.highlights[0].hostname, "mozilla.org");
       assert.equal(feed.highlights[0].hasImage, true);
     });
-    it("should add the image from the imageCache if it exists to the link", async () => {
-      links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
-      feed.imageCache = new Map([["https://mozilla.org", FAKE_IMAGE]]);
+    it("should add an existing image if it exists to the link without calling fetchImage", async () => {
+      links = [{url: "https://mozilla.org", image: FAKE_IMAGE}];
+      feed.highlights = links;
+      sinon.spy(feed, "fetchImage");
       await feed.fetchHighlights();
       assert.equal(feed.highlights[0].image, FAKE_IMAGE);
+      assert.notCalled(feed.fetchImage);
     });
-    it("should call fetchImage with the correct arguments for each link", async () => {
+    it("should call fetchImage with the correct arguments for new links", async () => {
       links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
+      feed.highlights = [];
       sinon.spy(feed, "fetchImage");
       await feed.fetchHighlights();
       assert.calledOnce(feed.fetchImage);
       assert.calledWith(feed.fetchImage, links[0].url, links[0].preview_image_url);
     });
     it("should not include any links already in Top Sites", async () => {
       links = [
         {url: "https://mozilla.org"},
@@ -161,24 +164,16 @@ describe("Highlights Feed", () => {
       assert.deepEqual(feed.highlights[0], links[0]);
       assert.deepEqual(feed.highlights[1], links[2]);
     });
     it("should set type to bookmark if there is a bookmarkGuid", async () => {
       links = [{url: "https://mozilla.org", type: "history", bookmarkGuid: "1234567890"}];
       await feed.fetchHighlights();
       assert.equal(feed.highlights[0].type, "bookmark");
     });
-    it("should clear the imageCache at the end", async () => {
-      links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jpg"}];
-      feed.imageCache = new Map([["https://mozilla.org", FAKE_IMAGE]]);
-      // Stops fetchImage adding to the cache
-      feed.fetchImage = () => {};
-      await feed.fetchHighlights();
-      assert.equal(feed.imageCache.size, 0);
-    });
     it("should not filter out adult pages when pref is false", async() => {
       await feed.fetchHighlights();
 
       assert.notCalled(filterAdultStub);
     });
     it("should filter out adult pages when pref is true", async() => {
       feed.store.state.Prefs.values.filterAdult = true;
 
@@ -197,26 +192,27 @@ describe("Highlights Feed", () => {
       assert.calledOnce(fakeScreenshot.getScreenshotForURL);
       assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_IMAGE_URL);
     });
     it("should fall back to capturing a screenshot", async () => {
       await feed.fetchImage(FAKE_URL, undefined);
       assert.calledOnce(fakeScreenshot.getScreenshotForURL);
       assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_URL);
     });
-    it("should store the image in the imageCache", async () => {
-      feed.imageCache.clear();
-      await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
-      assert.equal(feed.imageCache.get(FAKE_URL), FAKE_IMAGE);
-    });
     it("should call SectionsManager.updateSectionCard with the right arguments", async () => {
       await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
       assert.calledOnce(sectionsManagerStub.updateSectionCard);
       assert.calledWith(sectionsManagerStub.updateSectionCard, "highlights", FAKE_URL, {image: FAKE_IMAGE}, true);
     });
+    it("should update the card in feed.highlights with the image", async () => {
+      feed.highlights = [{url: FAKE_URL}];
+      await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
+      const highlight = feed.highlights.find(({url}) => url === FAKE_URL);
+      assert.propertyVal(highlight, "image", FAKE_IMAGE);
+    });
   });
   describe("#uninit", () => {
     it("should disable its section", () => {
       feed.onAction({type: at.UNINIT});
       assert.calledOnce(sectionsManagerStub.disableSection);
       assert.calledWith(sectionsManagerStub.disableSection, SECTION_ID);
     });
   });