diff --git a/pd/nw/dialog_prefs.html b/pd/nw/dialog_prefs.html
index e4abcae8da07a04db321bc1eb1ee69b56990bc64..07a857749594c6722cde64de2092bf787ecd773b 100644
--- a/pd/nw/dialog_prefs.html
+++ b/pd/nw/dialog_prefs.html
@@ -789,6 +789,12 @@ function apply(save_prefs) {
         get_bool_elem("browser_init"),
         get_autopatch_yoffset()
     );
+    // Also send the browser config to the GUI, so that it will update the
+    // help browser accordingly.
+    pdgui.update_browser(
+        get_bool_elem("browser_doc"),
+        get_bool_elem("browser_path")
+    );
     // Update the grid on all open windows.
     pdgui.update_grid(get_bool_elem("show_grid"));
 
diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js
index a23dc55d083141ea6d5e67f5c36ee8bf9f376450..de8a826bf5e42f30c84f76a6221cf9ead154b48b 100644
--- a/pd/nw/pdgui.js
+++ b/pd/nw/pdgui.js
@@ -74,6 +74,29 @@ exports.set_focused_patchwin = function(cid) {
     last_focused = cid;
 }
 
+// ico@vt.edu 2020-08-12: check which OS we have since Windows has a different
+// windows positioning logic on nw.js 0.14.7 than Linux/OSX. Go figure...
+function check_os(name) {
+
+    var os = require('os');
+    //post("os=" + os.platform());
+    return os.platform() === name ? 1 : 0;
+}
+
+exports.check_os = check_os;
+
+// ico@vt.edu 2020-08-14: used to speed-up window size consistency and other
+// OS-centric operations by avoiding constant calls to string comparisons.
+// Most pertinent calls can be found in index.js' nw_create_window and pdgui.js'
+// canvas_check_geometry
+var nw_os_is_linux = check_os("linux");
+var nw_os_is_osx = check_os("darwin");
+var nw_os_is_windows = check_os("win32");
+
+exports.nw_os_is_linux = nw_os_is_linux;
+exports.nw_os_is_osx = nw_os_is_osx;
+exports.nw_os_is_windows = nw_os_is_windows;
+
 // Keyword index (cf. dialog_search.html)
 
 var fs = require("fs");
@@ -81,14 +104,30 @@ var path = require("path");
 var dive = require("./dive.js"); // small module to recursively search dirs
 var elasticlunr = require("./elasticlunr.js"); // lightweight full-text search engine in JavaScript, cf. https://github.com/weixsong/elasticlunr.js/
 
-var index = elasticlunr();
-
-index.addField("title");
-index.addField("keywords");
-index.addField("description");
-//index.addField("body");
-index.addField("path");
-index.setRef("id");
+function init_elasticlunr()
+{
+    index = elasticlunr();
+    index.addField("title");
+    index.addField("keywords");
+    index.addField("description");
+    //index.addField("body");
+    index.addField("path");
+    index.setRef("id");
+    return index;
+}
+
+var index = init_elasticlunr();
+var index_cache = new Array();
+var index_manif = new Set();
+
+function index_entry_esc(s) {
+    if (s) {
+	var t = s.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
+	return t.replace(/(?:\r\n|\r|\n)/g, "\\n");
+    } else {
+	return "";
+    }
+}
 
 function add_doc_to_index(filename, data) {
     var title = path.basename(filename, ".pd"),
@@ -111,6 +150,12 @@ function add_doc_to_index(filename, data) {
     if (title.slice(-5) === "-help") {
         title = title.slice(0, -5);
     }
+    index_cache[index_cache.length] = [filename, title, keywords, desc]
+	.map(index_entry_esc).join(":");
+    var d = path.dirname(filename);
+    index_manif.add(d);
+    // Also add the parent directory to catch additions of siblings.
+    index_manif.add(path.dirname(d));
     index.addDoc({
         "id": filename,
         "title": title,
@@ -142,21 +187,82 @@ function read_file(err, filename, stat) {
 
 var index_done = false;
 var index_started = false;
+var index_start_time;
+
+// Filenames for the index cache, relative to the user's homedir.
+const cache_basename = nw_os_is_windows
+      ? "~/AppData/Roaming/Purr-Data/search"
+      : "~/.purr-data/search";
+const cache_name = cache_basename + ".index";
+const stamps_name = cache_basename + ".stamps";
 
 function finish_index() {
     index_done = true;
-    post("finished building help index");
+    var have_cache = index_cache.length > 0;
+    try {
+	// write the index cache if we have one
+	if (have_cache) {
+	    var a = new Array();
+	    index_manif.forEach(function(x) {
+		var st = fs.statSync(x);
+		a[a.length] = index_entry_esc(x) + ":" + st.mtimeMs;
+	    });
+	    a.sort();
+	    // Make sure that the target dir exists:
+	    try {
+		fs.mkdirSync(expand_tilde(path.dirname(cache_name)));
+	    } catch (err) {
+		//console.log(err);
+	    }
+	    fs.writeFileSync(expand_tilde(cache_name),
+			     index_cache.join("\n"), {mode: 0o644});
+	    // also write a manifest with the timestamps of all directories:
+	    fs.writeFileSync(expand_tilde(stamps_name),
+			     a.join("\n"), {mode: 0o644});
+	}
+    } catch (err) {
+	console.log(err);
+    }
+    var t = new Date().getTime() / 1000;
+    post("finished " + (have_cache?"building":"loading") + " help index (" +
+	 (t-index_start_time).toFixed(2) + " secs)");
 }
 
 // AG: pilfered from https://stackoverflow.com/questions/21077670
 function expand_tilde(filepath) {
     if (filepath[0] === '~') {
-        return path.join(process.env.HOME, filepath.slice(1));
+        var home = nw_os_is_windows ? process.env.HOMEPATH : process.env.HOME;
+        return path.join(home, filepath.slice(1));
     }
     return filepath;
 }
 
-// AG: This is supposed to be executed only once, after lib_dir has been set.
+function check_timestamps(manif)
+{
+    manif = manif.split('\n');
+    for (var j = 0, l = manif.length; j < l; j++) {
+	if (manif[j]) {
+	    var e = manif[j].replace(/\\:/g, "\x1c").split(':')
+		.map(x => x
+		     .replace(/\x1c/g, ":")
+		     .replace(/\\n/g, "\n")
+		     .replace(/\\\\/g, "\\"));
+	    var dirname = e[0] ? e[0] : null;
+	    var stamp = e[1] ? parseFloat(e[1]) : 0.0;
+	    try {
+		var st = fs.statSync(dirname);
+		if (st.mtimeMs > stamp) {
+		    return false;
+		}
+	    } catch (err) {
+		return false;
+	    }
+	}
+    }
+    return true;
+}
+
+// AG: This is normally executed only once, after lib_dir has been set.
 // Note that dive() traverses lib_dir asynchronously, so we report back in
 // finish_index() when this is done.
 function make_index() {
@@ -182,8 +288,47 @@ function make_index() {
         }
     }
     index_started = true;
-    post("building help index in " + doc_path);
-    dive(doc_path, read_file, browser_path?make_index_cont:finish_index);
+    index_start_time = new Date().getTime() / 1000;
+    var idx, manif;
+    try {
+	// test for index cache and manifest
+	idx = fs.readFileSync
+	(expand_tilde(cache_name), 'utf8');
+	manif = fs.readFileSync
+	(expand_tilde(stamps_name), 'utf8');
+    } catch (err) {
+	//console.log(err);
+    }
+    if (idx && manif && check_timestamps(manif)) {
+	// index cache is present and up-to-date, load it
+	post("loading cached help index from " + cache_name);
+	idx = idx.split('\n');
+	for (var j = 0, l = idx.length; j < l; j++) {
+	    if (idx[j]) {
+		var e = idx[j].replace(/\\:/g, "\x1c").split(':')
+		    .map(x => x
+			 .replace(/\x1c/g, ":")
+			 .replace(/\\n/g, "\n")
+			 .replace(/\\\\/g, "\\"));
+		var filename = e[0] ? e[0] : null;
+		var title = e[1] ? e[1] : null;
+		var keywords = e[2] ? e[2] : null;
+		var descr = e[3] ? e[3] : null;
+		index.addDoc({
+		    "id": filename,
+		    "title": title,
+		    "keywords": keywords,
+		    "description": descr
+		});
+	    }
+	}
+        finish_index();
+    } else {
+	// no index cache, or it is out of date, so (re)build it now, and
+	// save the new cache along the way
+	post("building help index in " + doc_path);
+	dive(doc_path, read_file, browser_path?make_index_cont:finish_index);
+    }
 }
 
 // AG: This is called from dialog_search.html with a callback that expects to
@@ -206,6 +351,42 @@ function build_index(cb) {
 
 exports.build_index = build_index;
 
+// this doesn't actually rebuild the index, it just clears it, so that it
+// will be rebuilt the next time the help browser is opened
+function rebuild_index()
+{
+    post("clearing help index (reopen browser to rebuild!)");
+    index = init_elasticlunr();
+    index_started = index_done = false;
+    try {
+	fs.unlink(expand_tilde(cache_name));
+	fs.unlink(expand_tilde(stamps_name));
+    } catch (err) {
+	//console.log(err);
+    }
+}
+
+// this is called from the gui tab of the prefs dialog
+function update_browser(doc_flag, path_flag)
+{
+    var changed = false;
+    doc_flag = doc_flag?1:0;
+    path_flag = path_flag?1:0;
+    if (browser_doc !== doc_flag) {
+	browser_doc = doc_flag;
+	changed = true;
+    }
+    if (browser_path !== path_flag) {
+	browser_path = path_flag;
+	changed = true;
+    }
+    if (changed) {
+	rebuild_index();
+    }
+}
+
+exports.update_browser = update_browser;
+
 // Modules
 
 var cp = require("child_process"); // for starting core Pd from GUI in OSX
@@ -815,29 +996,6 @@ function check_nw_version(version) {
 
 exports.check_nw_version = check_nw_version;
 
-// ico@vt.edu 2020-08-12: check which OS we have since Windows has a different
-// windows positioning logic on nw.js 0.14.7 than Linux/OSX. Go figure...
-function check_os(name) {
-
-    var os = require('os');
-    //post("os=" + os.platform());
-    return os.platform() === name ? 1 : 0;
-}
-
-exports.check_os = check_os;
-
-// ico@vt.edu 2020-08-14: used to speed-up window size consistency and other
-// OS-centric operations by avoiding constant calls to string comparisons.
-// Most pertinent calls can be found in index.js' nw_create_window and pdgui.js'
-// canvas_check_geometry
-var nw_os_is_linux = check_os("linux");
-var nw_os_is_osx = check_os("darwin");
-var nw_os_is_windows = check_os("win32");
-
-exports.nw_os_is_linux = nw_os_is_linux;
-exports.nw_os_is_osx = nw_os_is_osx;
-exports.nw_os_is_windows = nw_os_is_windows;
-
 // ico@vt.edu 2020-08-11: this appears to have to be 25 at all times
 // we will leave this here for later if we encounter issues with inconsistencies
 // across different nw.js versions...